mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2024-11-14 05:35:06 +01:00
merge md2 stuff in
initial changes for tracking
This commit is contained in:
commit
f83a6bd489
@ -21,14 +21,14 @@ import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
|||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
import eu.kanade.tachiyomi.ui.manga.MangaDetailsController
|
||||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import eu.kanade.tachiyomi.util.system.notificationManager
|
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
||||||
@ -405,7 +405,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
|||||||
val newIntent =
|
val newIntent =
|
||||||
Intent(context, MainActivity::class.java).setAction(MainActivity.SHORTCUT_MANGA)
|
Intent(context, MainActivity::class.java).setAction(MainActivity.SHORTCUT_MANGA)
|
||||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||||
.putExtra(MangaController.MANGA_EXTRA, manga.id)
|
.putExtra(MangaDetailsController.MANGA_EXTRA, manga.id)
|
||||||
.putExtra("notificationId", manga.id.hashCode())
|
.putExtra("notificationId", manga.id.hashCode())
|
||||||
.putExtra("groupId", groupId)
|
.putExtra("groupId", groupId)
|
||||||
return PendingIntent.getActivity(
|
return PendingIntent.getActivity(
|
||||||
|
@ -145,6 +145,8 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
const val keepCatSort = "keep_cat_sort"
|
const val keepCatSort = "keep_cat_sort"
|
||||||
|
|
||||||
|
const val alwaysShowChapterTransition = "always_show_chapter_transition"
|
||||||
|
|
||||||
@Deprecated("Use the preferences of the source")
|
@Deprecated("Use the preferences of the source")
|
||||||
fun sourceUsername(sourceId: Long) = "pref_source_username_$sourceId"
|
fun sourceUsername(sourceId: Long) = "pref_source_username_$sourceId"
|
||||||
|
|
||||||
@ -153,6 +155,7 @@ object PreferenceKeys {
|
|||||||
|
|
||||||
fun sourceSharedPref(sourceId: Long) = "source_$sourceId"
|
fun sourceSharedPref(sourceId: Long) = "source_$sourceId"
|
||||||
|
|
||||||
|
|
||||||
fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId"
|
fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId"
|
||||||
|
|
||||||
fun trackPassword(syncId: Int) = "pref_mangasync_password_$syncId"
|
fun trackPassword(syncId: Int) = "pref_mangasync_password_$syncId"
|
||||||
|
@ -248,4 +248,6 @@ class PreferencesHelper(val context: Context) {
|
|||||||
fun keepCatSort() = rxPrefs.getInteger(Keys.keepCatSort, 0)
|
fun keepCatSort() = rxPrefs.getInteger(Keys.keepCatSort, 0)
|
||||||
|
|
||||||
fun hideFiltersAtStart() = rxPrefs.getBoolean("hide_filters_at_start", false)
|
fun hideFiltersAtStart() = rxPrefs.getBoolean("hide_filters_at_start", false)
|
||||||
|
|
||||||
|
fun alwaysShowChapterTransition() = rxPrefs.getBoolean(Keys.alwaysShowChapterTransition, true)
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ import eu.kanade.tachiyomi.data.track.myanimelist.Myanimelist
|
|||||||
import eu.kanade.tachiyomi.data.track.shikimori.Shikimori
|
import eu.kanade.tachiyomi.data.track.shikimori.Shikimori
|
||||||
import eu.kanade.tachiyomi.data.track.bangumi.Bangumi
|
import eu.kanade.tachiyomi.data.track.bangumi.Bangumi
|
||||||
|
|
||||||
class TrackManager(private val context: Context) {
|
class TrackManager(context: Context) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val MYANIMELIST = 1
|
const val MYANIMELIST = 1
|
||||||
|
@ -7,8 +7,6 @@ import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
|||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import rx.Completable
|
|
||||||
import rx.Observable
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
abstract class TrackService(val id: Int) {
|
abstract class TrackService(val id: Int) {
|
||||||
@ -39,17 +37,17 @@ abstract class TrackService(val id: Int) {
|
|||||||
|
|
||||||
abstract fun displayScore(track: Track): String
|
abstract fun displayScore(track: Track): String
|
||||||
|
|
||||||
abstract fun add(track: Track): Observable<Track>
|
abstract suspend fun add(track: Track): Track
|
||||||
|
|
||||||
abstract fun update(track: Track): Observable<Track>
|
abstract suspend fun update(track: Track): Track
|
||||||
|
|
||||||
abstract fun bind(track: Track): Observable<Track>
|
abstract suspend fun bind(track: Track): Track
|
||||||
|
|
||||||
abstract fun search(query: String): Observable<List<TrackSearch>>
|
abstract suspend fun search(query: String): List<TrackSearch>
|
||||||
|
|
||||||
abstract fun refresh(track: Track): Observable<Track>
|
abstract suspend fun refresh(track: Track): Track
|
||||||
|
|
||||||
abstract fun login(username: String, password: String): Completable
|
abstract suspend fun login(username: String, password: String): Boolean
|
||||||
|
|
||||||
@CallSuper
|
@CallSuper
|
||||||
open fun logout() {
|
open fun logout() {
|
||||||
|
@ -8,8 +8,6 @@ import eu.kanade.tachiyomi.data.database.models.Track
|
|||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import rx.Completable
|
|
||||||
import rx.Observable
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class Anilist(private val context: Context, id: Int) : TrackService(id) {
|
class Anilist(private val context: Context, id: Int) : TrackService(id) {
|
||||||
@ -128,68 +126,69 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun add(track: Track): Observable<Track> {
|
override suspend fun add(track: Track): Track {
|
||||||
return api.addLibManga(track)
|
return api.addLibManga(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun update(track: Track): Observable<Track> {
|
override suspend fun update(track: Track): Track {
|
||||||
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
|
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
|
||||||
track.status = COMPLETED
|
track.status = COMPLETED
|
||||||
}
|
}
|
||||||
// If user was using API v1 fetch library_id
|
// If user was using API v1 fetch library_id
|
||||||
if (track.library_id == null || track.library_id!! == 0L){
|
if (track.library_id == null || track.library_id!! == 0L) {
|
||||||
return api.findLibManga(track, getUsername().toInt()).flatMap {
|
val libManga = api.findLibManga(track, getUsername().toInt())
|
||||||
if (it == null) {
|
|
||||||
|
if (libManga == null) {
|
||||||
throw Exception("$track not found on user library")
|
throw Exception("$track not found on user library")
|
||||||
}
|
}
|
||||||
track.library_id = it.library_id
|
track.library_id = libManga.library_id
|
||||||
api.updateLibManga(track)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return api.updateLibManga(track)
|
return api.updateLibManga(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun bind(track: Track): Observable<Track> {
|
override suspend fun bind(track: Track): Track {
|
||||||
return api.findLibManga(track, getUsername().toInt())
|
val remoteTrack = api.findLibManga(track, getUsername().toInt())
|
||||||
.flatMap { remoteTrack ->
|
|
||||||
if (remoteTrack != null) {
|
if (remoteTrack != null) {
|
||||||
track.copyPersonalFrom(remoteTrack)
|
track.copyPersonalFrom(remoteTrack)
|
||||||
track.library_id = remoteTrack.library_id
|
track.library_id = remoteTrack.library_id
|
||||||
update(track)
|
return update(track)
|
||||||
} else {
|
} else {
|
||||||
// Set default fields if it's not found in the list
|
// Set default fields if it's not found in the list
|
||||||
track.score = DEFAULT_SCORE.toFloat()
|
track.score = DEFAULT_SCORE.toFloat()
|
||||||
track.status = DEFAULT_STATUS
|
track.status = DEFAULT_STATUS
|
||||||
add(track)
|
return add(track)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun search(query: String): Observable<List<TrackSearch>> {
|
override suspend fun search(query: String): List<TrackSearch> {
|
||||||
return api.search(query)
|
return api.search(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun refresh(track: Track): Observable<Track> {
|
override suspend fun refresh(track: Track): Track {
|
||||||
return api.getLibManga(track, getUsername().toInt())
|
val remoteTrack = api.getLibManga(track, getUsername().toInt())
|
||||||
.map { remoteTrack ->
|
|
||||||
track.copyPersonalFrom(remoteTrack)
|
track.copyPersonalFrom(remoteTrack)
|
||||||
track.total_chapters = remoteTrack.total_chapters
|
track.total_chapters = remoteTrack.total_chapters
|
||||||
track
|
return track
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun login(username: String, password: String) = login(password)
|
override suspend fun login(username: String, password: String) = login(password)
|
||||||
|
|
||||||
fun login(token: String): Completable {
|
suspend fun login(token: String): Boolean {
|
||||||
val oauth = api.createOAuth(token)
|
val oauth = api.createOAuth(token)
|
||||||
interceptor.setAuth(oauth)
|
interceptor.setAuth(oauth)
|
||||||
return api.getCurrentUser().map { (username, scoreType) ->
|
|
||||||
scorePreference.set(scoreType)
|
try {
|
||||||
saveCredentials(username.toString(), oauth.access_token)
|
val currentUser = api.getCurrentUser()
|
||||||
}.doOnError{
|
scorePreference.set(currentUser.second)
|
||||||
|
saveCredentials(currentUser.first.toString(), oauth.access_token)
|
||||||
|
return true
|
||||||
|
} catch (e: Exception) {
|
||||||
logout()
|
logout()
|
||||||
}.toCompletable()
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun logout() {
|
override fun logout() {
|
||||||
|
@ -11,24 +11,19 @@ import com.google.gson.JsonObject
|
|||||||
import com.google.gson.JsonParser
|
import com.google.gson.JsonParser
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
import eu.kanade.tachiyomi.network.await
|
||||||
import okhttp3.MediaType
|
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody
|
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import rx.Observable
|
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
|
|
||||||
|
|
||||||
class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||||
|
|
||||||
private val parser = JsonParser()
|
|
||||||
private val jsonMime = "application/json; charset=utf-8".toMediaTypeOrNull()
|
private val jsonMime = "application/json; charset=utf-8".toMediaTypeOrNull()
|
||||||
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
||||||
|
|
||||||
fun addLibManga(track: Track): Observable<Track> {
|
suspend fun addLibManga(track: Track): Track {
|
||||||
val query = """
|
val query = """
|
||||||
|mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) {
|
|mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) {
|
||||||
|SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) {
|
|SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) {
|
||||||
@ -51,21 +46,20 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
.url(apiUrl)
|
.url(apiUrl)
|
||||||
.post(body)
|
.post(body)
|
||||||
.build()
|
.build()
|
||||||
return authClient.newCall(request)
|
val netResponse = authClient.newCall(request).await()
|
||||||
.asObservableSuccess()
|
|
||||||
.map { netResponse ->
|
|
||||||
val responseBody = netResponse.body?.string().orEmpty()
|
val responseBody = netResponse.body?.string().orEmpty()
|
||||||
netResponse.close()
|
netResponse.close()
|
||||||
if (responseBody.isEmpty()) {
|
if (responseBody.isEmpty()) {
|
||||||
throw Exception("Null Response")
|
throw Exception("Null Response")
|
||||||
}
|
}
|
||||||
val response = parser.parse(responseBody).obj
|
val response = JsonParser().parse(responseBody).obj
|
||||||
track.library_id = response["data"]["SaveMediaListEntry"]["id"].asLong
|
track.library_id = response["data"]["SaveMediaListEntry"]["id"].asLong
|
||||||
track
|
|
||||||
}
|
return track
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateLibManga(track: Track): Observable<Track> {
|
suspend fun updateLibManga(track: Track): Track {
|
||||||
val query = """
|
val query = """
|
||||||
|mutation UpdateManga(${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}score: Int) {
|
|mutation UpdateManga(${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}score: Int) {
|
||||||
|SaveMediaListEntry (id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, scoreRaw: ${'$'}score) {
|
|SaveMediaListEntry (id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, scoreRaw: ${'$'}score) {
|
||||||
@ -90,14 +84,11 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
.url(apiUrl)
|
.url(apiUrl)
|
||||||
.post(body)
|
.post(body)
|
||||||
.build()
|
.build()
|
||||||
return authClient.newCall(request)
|
authClient.newCall(request).execute()
|
||||||
.asObservableSuccess()
|
return track
|
||||||
.map {
|
|
||||||
track
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun search(search: String): Observable<List<TrackSearch>> {
|
suspend fun search(search: String): List<TrackSearch> {
|
||||||
val query = """
|
val query = """
|
||||||
|query Search(${'$'}query: String) {
|
|query Search(${'$'}query: String) {
|
||||||
|Page (perPage: 50) {
|
|Page (perPage: 50) {
|
||||||
@ -134,24 +125,20 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
.url(apiUrl)
|
.url(apiUrl)
|
||||||
.post(body)
|
.post(body)
|
||||||
.build()
|
.build()
|
||||||
return authClient.newCall(request)
|
val netResponse = authClient.newCall(request).await()
|
||||||
.asObservableSuccess()
|
|
||||||
.map { netResponse ->
|
|
||||||
val responseBody = netResponse.body?.string().orEmpty()
|
val responseBody = netResponse.body?.string().orEmpty()
|
||||||
if (responseBody.isEmpty()) {
|
if (responseBody.isEmpty()) {
|
||||||
throw Exception("Null Response")
|
throw Exception("Null Response")
|
||||||
}
|
}
|
||||||
val response = parser.parse(responseBody).obj
|
val response = JsonParser().parse(responseBody).obj
|
||||||
val data = response["data"]!!.obj
|
val data = response["data"]!!.obj
|
||||||
val page = data["Page"].obj
|
val page = data["Page"].obj
|
||||||
val media = page["media"].array
|
val media = page["media"].array
|
||||||
val entries = media.map { jsonToALManga(it.obj) }
|
val entries = media.map { jsonToALManga(it.obj) }
|
||||||
entries.map { it.toTrack() }
|
return entries.map { it.toTrack() }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun findLibManga(track: Track, userid: Int): Track? {
|
||||||
fun findLibManga(track: Track, userid: Int): Observable<Track?> {
|
|
||||||
val query = """
|
val query = """
|
||||||
|query (${'$'}id: Int!, ${'$'}manga_id: Int!) {
|
|query (${'$'}id: Int!, ${'$'}manga_id: Int!) {
|
||||||
|Page {
|
|Page {
|
||||||
@ -195,33 +182,35 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
.url(apiUrl)
|
.url(apiUrl)
|
||||||
.post(body)
|
.post(body)
|
||||||
.build()
|
.build()
|
||||||
return authClient.newCall(request)
|
val result = authClient.newCall(request).await()
|
||||||
.asObservableSuccess()
|
return result.let { resp ->
|
||||||
.map { netResponse ->
|
val responseBody = resp.body?.string().orEmpty()
|
||||||
val responseBody = netResponse.body?.string().orEmpty()
|
|
||||||
if (responseBody.isEmpty()) {
|
if (responseBody.isEmpty()) {
|
||||||
throw Exception("Null Response")
|
throw Exception("Null Response")
|
||||||
}
|
}
|
||||||
val response = parser.parse(responseBody).obj
|
val response = JsonParser().parse(responseBody).obj
|
||||||
val data = response["data"]!!.obj
|
val data = response["data"]!!.obj
|
||||||
val page = data["Page"].obj
|
val page = data["Page"].obj
|
||||||
val media = page["mediaList"].array
|
val media = page["mediaList"].array
|
||||||
val entries = media.map { jsonToALUserManga(it.obj) }
|
val entries = media.map { jsonToALUserManga(it.obj) }
|
||||||
entries.firstOrNull()?.toTrack()
|
entries.firstOrNull()?.toTrack()
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getLibManga(track: Track, userid: Int): Observable<Track> {
|
suspend fun getLibManga(track: Track, userid: Int): Track {
|
||||||
return findLibManga(track, userid)
|
val track = findLibManga(track, userid)
|
||||||
.map { it ?: throw Exception("Could not find manga") }
|
if (track == null) {
|
||||||
|
throw Exception("Could not find manga")
|
||||||
|
} else {
|
||||||
|
return track
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun createOAuth(token: String): OAuth {
|
fun createOAuth(token: String): OAuth {
|
||||||
return OAuth(token, "Bearer", System.currentTimeMillis() + 31536000000, 31536000000)
|
return OAuth(token, "Bearer", System.currentTimeMillis() + 31536000000, 31536000000)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCurrentUser(): Observable<Pair<Int, String>> {
|
suspend fun getCurrentUser(): Pair<Int, String> {
|
||||||
val query = """
|
val query = """
|
||||||
|query User {
|
|query User {
|
||||||
|Viewer {
|
|Viewer {
|
||||||
@ -240,42 +229,55 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
.url(apiUrl)
|
.url(apiUrl)
|
||||||
.post(body)
|
.post(body)
|
||||||
.build()
|
.build()
|
||||||
return authClient.newCall(request)
|
val netResponse = authClient.newCall(request).await()
|
||||||
.asObservableSuccess()
|
|
||||||
.map { netResponse ->
|
|
||||||
val responseBody = netResponse.body?.string().orEmpty()
|
val responseBody = netResponse.body?.string().orEmpty()
|
||||||
if (responseBody.isEmpty()) {
|
if (responseBody.isEmpty()) {
|
||||||
throw Exception("Null Response")
|
throw Exception("Null Response")
|
||||||
}
|
}
|
||||||
val response = parser.parse(responseBody).obj
|
val response = JsonParser().parse(responseBody).obj
|
||||||
val data = response["data"]!!.obj
|
val data = response["data"]!!.obj
|
||||||
val viewer = data["Viewer"].obj
|
val viewer = data["Viewer"].obj
|
||||||
Pair(viewer["id"].asInt, viewer["mediaListOptions"]["scoreFormat"].asString)
|
return Pair(viewer["id"].asInt, viewer["mediaListOptions"]["scoreFormat"].asString)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun jsonToALManga(struct: JsonObject): ALManga {
|
private fun jsonToALManga(struct: JsonObject): ALManga {
|
||||||
val date = try {
|
val date = try {
|
||||||
val date = Calendar.getInstance()
|
val date = Calendar.getInstance()
|
||||||
date.set(struct["startDate"]["year"].nullInt ?: 0, (struct["startDate"]["month"].nullInt ?: 0) - 1,
|
date.set(
|
||||||
struct["startDate"]["day"].nullInt ?: 0)
|
struct["startDate"]["year"].nullInt ?: 0,
|
||||||
|
(struct["startDate"]["month"].nullInt ?: 0) - 1,
|
||||||
|
struct["startDate"]["day"].nullInt ?: 0
|
||||||
|
)
|
||||||
date.timeInMillis
|
date.timeInMillis
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
0L
|
0L
|
||||||
}
|
}
|
||||||
|
|
||||||
return ALManga(struct["id"].asInt, struct["title"]["romaji"].asString, struct["coverImage"]["large"].asString,
|
return ALManga(
|
||||||
struct["description"].nullString.orEmpty(), struct["type"].asString, struct["status"].asString,
|
struct["id"].asInt,
|
||||||
date, struct["chapters"].nullInt ?: 0)
|
struct["title"]["romaji"].asString,
|
||||||
|
struct["coverImage"]["large"].asString,
|
||||||
|
struct["description"].nullString.orEmpty(),
|
||||||
|
struct["type"].asString,
|
||||||
|
struct["status"].asString,
|
||||||
|
date,
|
||||||
|
struct["chapters"].nullInt ?: 0
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun jsonToALUserManga(struct: JsonObject): ALUserManga {
|
private fun jsonToALUserManga(struct: JsonObject): ALUserManga {
|
||||||
return ALUserManga(struct["id"].asLong, struct["status"].asString, struct["scoreRaw"].asInt, struct["progress"].asInt, jsonToALManga(struct["media"].obj))
|
return ALUserManga(
|
||||||
|
struct["id"].asLong,
|
||||||
|
struct["status"].asString,
|
||||||
|
struct["scoreRaw"].asInt,
|
||||||
|
struct["progress"].asInt,
|
||||||
|
jsonToALManga(struct["media"].obj)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val clientId = "385"
|
private const val clientId = "385"
|
||||||
private const val clientUrl = "tachiyomi://anilist-auth"
|
|
||||||
private const val apiUrl = "https://graphql.anilist.co/"
|
private const val apiUrl = "https://graphql.anilist.co/"
|
||||||
private const val baseUrl = "https://anilist.co/api/v2/"
|
private const val baseUrl = "https://anilist.co/api/v2/"
|
||||||
private const val baseMangaUrl = "https://anilist.co/manga/"
|
private const val baseMangaUrl = "https://anilist.co/manga/"
|
||||||
@ -289,5 +291,4 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||||||
.appendQueryParameter("response_type", "token")
|
.appendQueryParameter("response_type", "token")
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,13 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.anilist
|
package eu.kanade.tachiyomi.data.track.anilist
|
||||||
|
|
||||||
import android.app.DownloadManager
|
|
||||||
import android.content.Context
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.Locale
|
||||||
|
|
||||||
data class ALManga(
|
data class ALManga(
|
||||||
val media_id: Int,
|
val media_id: Int,
|
||||||
@ -49,8 +44,7 @@ data class ALUserManga(
|
|||||||
val list_status: String,
|
val list_status: String,
|
||||||
val score_raw: Int,
|
val score_raw: Int,
|
||||||
val chapters_read: Int,
|
val chapters_read: Int,
|
||||||
val manga: ALManga,
|
val manga: ALManga
|
||||||
val context: Context = Injekt.get<PreferencesHelper>().context
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun toTrack() = Track.create(TrackManager.ANILIST).apply {
|
fun toTrack() = Track.create(TrackManager.ANILIST).apply {
|
||||||
@ -62,17 +56,15 @@ data class ALUserManga(
|
|||||||
total_chapters = manga.total_chapters
|
total_chapters = manga.total_chapters
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toTrackStatus() = with(context) {
|
fun toTrackStatus() = when (list_status) {
|
||||||
when (list_status) {
|
"CURRENT" -> Anilist.READING
|
||||||
getString(R.string.reading) -> Anilist.READING
|
"COMPLETED" -> Anilist.COMPLETED
|
||||||
getString(R.string.completed) -> Anilist.COMPLETED
|
"PAUSED" -> Anilist.PAUSED
|
||||||
getString(R.string.paused) -> Anilist.PAUSED
|
"DROPPED" -> Anilist.DROPPED
|
||||||
getString(R.string.dropped) -> Anilist.DROPPED
|
"PLANNING" -> Anilist.PLANNING
|
||||||
getString(R.string.plan_to_read) -> Anilist.PLANNING
|
"REPEATING" -> Anilist.REPEATING
|
||||||
getString(R.string.repeating)-> Anilist.REPEATING
|
|
||||||
else -> throw NotImplementedError("Unknown status")
|
else -> throw NotImplementedError("Unknown status")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Track.toAnilistStatus() = when (status) {
|
fun Track.toAnilistStatus() = when (status) {
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.bangumi
|
|
||||||
|
|
||||||
data class Avatar(
|
|
||||||
val large: String? = "",
|
|
||||||
val medium: String? = "",
|
|
||||||
val small: String? = ""
|
|
||||||
)
|
|
@ -7,8 +7,7 @@ import eu.kanade.tachiyomi.R
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import rx.Completable
|
import timber.log.Timber
|
||||||
import rx.Observable
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class Bangumi(private val context: Context, id: Int) : TrackService(id) {
|
class Bangumi(private val context: Context, id: Int) : TrackService(id) {
|
||||||
@ -29,55 +28,48 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
return track.score.toInt().toString()
|
return track.score.toInt().toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun add(track: Track): Observable<Track> {
|
override suspend fun add(track: Track): Track {
|
||||||
return api.addLibManga(track)
|
return api.addLibManga(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun update(track: Track): Observable<Track> {
|
override suspend fun update(track: Track): Track {
|
||||||
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
|
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
|
||||||
track.status = COMPLETED
|
track.status = COMPLETED
|
||||||
}
|
}
|
||||||
return api.updateLibManga(track)
|
return api.updateLibManga(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun bind(track: Track): Observable<Track> {
|
override suspend fun bind(track: Track): Track {
|
||||||
return api.statusLibManga(track)
|
val statusTrack = api.statusLibManga(track)
|
||||||
.flatMap {
|
val remoteTrack = api.findLibManga(track)
|
||||||
api.findLibManga(track).flatMap { remoteTrack ->
|
if (statusTrack != null && remoteTrack != null) {
|
||||||
if (remoteTrack != null && it != null) {
|
|
||||||
track.copyPersonalFrom(remoteTrack)
|
track.copyPersonalFrom(remoteTrack)
|
||||||
track.library_id = remoteTrack.library_id
|
track.library_id = remoteTrack.library_id
|
||||||
track.status = remoteTrack.status
|
track.status = remoteTrack.status
|
||||||
track.last_chapter_read = remoteTrack.last_chapter_read
|
track.last_chapter_read = remoteTrack.last_chapter_read
|
||||||
refresh(track)
|
refresh(track)
|
||||||
} else {
|
} else {
|
||||||
// Set default fields if it's not found in the list
|
|
||||||
track.score = DEFAULT_SCORE.toFloat()
|
track.score = DEFAULT_SCORE.toFloat()
|
||||||
track.status = DEFAULT_STATUS
|
track.status = DEFAULT_STATUS
|
||||||
add(track)
|
add(track)
|
||||||
update(track)
|
update(track)
|
||||||
}
|
}
|
||||||
}
|
return track
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun search(query: String): Observable<List<TrackSearch>> {
|
override suspend fun search(query: String): List<TrackSearch> {
|
||||||
return api.search(query)
|
return api.search(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun refresh(track: Track): Observable<Track> {
|
override suspend fun refresh(track: Track): Track {
|
||||||
return api.statusLibManga(track)
|
val statusTrack = api.statusLibManga(track)
|
||||||
.flatMap {
|
track.copyPersonalFrom(statusTrack!!)
|
||||||
track.copyPersonalFrom(it!!)
|
val remoteTrack = api.findLibManga(track)
|
||||||
api.findLibManga(track)
|
if(remoteTrack != null){
|
||||||
.map { remoteTrack ->
|
|
||||||
if (remoteTrack != null) {
|
|
||||||
track.total_chapters = remoteTrack.total_chapters
|
track.total_chapters = remoteTrack.total_chapters
|
||||||
track.status = remoteTrack.status
|
track.status = remoteTrack.status
|
||||||
}
|
}
|
||||||
track
|
return track
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getLogo() = R.drawable.tracker_bangumi
|
override fun getLogo() = R.drawable.tracker_bangumi
|
||||||
@ -99,17 +91,20 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun login(username: String, password: String) = login(password)
|
override suspend fun login(username: String, password: String): Boolean = login(password)
|
||||||
|
|
||||||
fun login(code: String): Completable {
|
suspend fun login(code: String): Boolean {
|
||||||
return api.accessToken(code).map { oauth: OAuth? ->
|
try {
|
||||||
|
|
||||||
|
val oauth = api.accessToken(code)
|
||||||
interceptor.newAuth(oauth)
|
interceptor.newAuth(oauth)
|
||||||
if (oauth != null) {
|
|
||||||
saveCredentials(oauth.user_id.toString(), oauth.access_token)
|
saveCredentials(oauth.user_id.toString(), oauth.access_token)
|
||||||
}
|
return true
|
||||||
}.doOnError {
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
logout()
|
logout()
|
||||||
}.toCompletable()
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveToken(oauth: OAuth?) {
|
fun saveToken(oauth: OAuth?) {
|
||||||
@ -128,7 +123,7 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
override fun logout() {
|
override fun logout() {
|
||||||
super.logout()
|
super.logout()
|
||||||
preferences.trackToken(this).set(null)
|
preferences.trackToken(this).set(null)
|
||||||
interceptor.newAuth(null)
|
interceptor.clearOauth()
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -10,22 +10,22 @@ import eu.kanade.tachiyomi.data.database.models.Track
|
|||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.network.POST
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
import eu.kanade.tachiyomi.network.await
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import okhttp3.CacheControl
|
import okhttp3.CacheControl
|
||||||
import okhttp3.FormBody
|
import okhttp3.FormBody
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import rx.Observable
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
|
|
||||||
class BangumiApi(private val client: OkHttpClient, interceptor: BangumiInterceptor) {
|
class BangumiApi(private val client: OkHttpClient, interceptor: BangumiInterceptor) {
|
||||||
|
|
||||||
private val gson: Gson by injectLazy()
|
private val gson: Gson by injectLazy()
|
||||||
private val parser = JsonParser()
|
|
||||||
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
||||||
|
|
||||||
fun addLibManga(track: Track): Observable<Track> {
|
suspend fun addLibManga(track: Track): Track {
|
||||||
val body = FormBody.Builder()
|
val body = FormBody.Builder()
|
||||||
.add("rating", track.score.toInt().toString())
|
.add("rating", track.score.toInt().toString())
|
||||||
.add("status", track.toBangumiStatus())
|
.add("status", track.toBangumiStatus())
|
||||||
@ -34,15 +34,13 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
|
|||||||
.url("$apiUrl/collection/${track.media_id}/update")
|
.url("$apiUrl/collection/${track.media_id}/update")
|
||||||
.post(body)
|
.post(body)
|
||||||
.build()
|
.build()
|
||||||
return authClient.newCall(request)
|
val response = authClient.newCall(request).await()
|
||||||
.asObservableSuccess()
|
return track
|
||||||
.map {
|
|
||||||
track
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateLibManga(track: Track): Observable<Track> {
|
suspend fun updateLibManga(track: Track): Track {
|
||||||
// chapter update
|
// chapter update
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
val body = FormBody.Builder()
|
val body = FormBody.Builder()
|
||||||
.add("watched_eps", track.last_chapter_read.toString())
|
.add("watched_eps", track.last_chapter_read.toString())
|
||||||
.build()
|
.build()
|
||||||
@ -59,31 +57,25 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
|
|||||||
.url("$apiUrl/collection/${track.media_id}/update")
|
.url("$apiUrl/collection/${track.media_id}/update")
|
||||||
.post(sbody)
|
.post(sbody)
|
||||||
.build()
|
.build()
|
||||||
return authClient.newCall(srequest)
|
authClient.newCall(srequest).execute()
|
||||||
.asObservableSuccess()
|
authClient.newCall(request).execute()
|
||||||
.map {
|
|
||||||
track
|
track
|
||||||
}.flatMap {
|
|
||||||
authClient.newCall(request)
|
|
||||||
.asObservableSuccess()
|
|
||||||
.map {
|
|
||||||
track
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun search(search: String): Observable<List<TrackSearch>> {
|
suspend fun search(search: String): List<TrackSearch> {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
val url = Uri.parse(
|
val url = Uri.parse(
|
||||||
"$apiUrl/search/subject/${URLEncoder.encode(search, Charsets.UTF_8.name())}").buildUpon()
|
"$apiUrl/search/subject/${URLEncoder.encode(search, Charsets.UTF_8.name())}"
|
||||||
|
).buildUpon()
|
||||||
.appendQueryParameter("max_results", "20")
|
.appendQueryParameter("max_results", "20")
|
||||||
.build()
|
.build()
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
.url(url.toString())
|
.url(url.toString())
|
||||||
.get()
|
.get()
|
||||||
.build()
|
.build()
|
||||||
return authClient.newCall(request)
|
|
||||||
.asObservableSuccess()
|
val netResponse = authClient.newCall(request).await()
|
||||||
.map { netResponse ->
|
|
||||||
var responseBody = netResponse.body?.string().orEmpty()
|
var responseBody = netResponse.body?.string().orEmpty()
|
||||||
if (responseBody.isEmpty()) {
|
if (responseBody.isEmpty()) {
|
||||||
throw Exception("Null Response")
|
throw Exception("Null Response")
|
||||||
@ -91,10 +83,13 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
|
|||||||
if (responseBody.contains("\"code\":404")) {
|
if (responseBody.contains("\"code\":404")) {
|
||||||
responseBody = "{\"results\":0,\"list\":[]}"
|
responseBody = "{\"results\":0,\"list\":[]}"
|
||||||
}
|
}
|
||||||
val response = parser.parse(responseBody).obj["list"]?.array
|
val response = JsonParser.parseString(responseBody).obj["list"]?.array
|
||||||
response?.filter { it.obj["type"].asInt == 1 }?.map { jsonToSearch(it.obj) }
|
if (response != null) {
|
||||||
|
response.filter { it.obj["type"].asInt == 1 }?.map { jsonToSearch(it.obj) }
|
||||||
|
} else {
|
||||||
|
listOf()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun jsonToSearch(obj: JsonObject): TrackSearch {
|
private fun jsonToSearch(obj: JsonObject): TrackSearch {
|
||||||
@ -119,23 +114,20 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun findLibManga(track: Track): Observable<Track?> {
|
suspend fun findLibManga(track: Track): Track? {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
val urlMangas = "$apiUrl/subject/${track.media_id}"
|
val urlMangas = "$apiUrl/subject/${track.media_id}"
|
||||||
val requestMangas = Request.Builder()
|
val requestMangas = Request.Builder()
|
||||||
.url(urlMangas)
|
.url(urlMangas)
|
||||||
.get()
|
.get()
|
||||||
.build()
|
.build()
|
||||||
|
val netResponse = authClient.newCall(requestMangas).execute()
|
||||||
return authClient.newCall(requestMangas)
|
|
||||||
.asObservableSuccess()
|
|
||||||
.map { netResponse ->
|
|
||||||
// get comic info
|
|
||||||
val responseBody = netResponse.body?.string().orEmpty()
|
val responseBody = netResponse.body?.string().orEmpty()
|
||||||
jsonToTrack(parser.parse(responseBody).obj)
|
jsonToTrack(JsonParser.parseString(responseBody).obj)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun statusLibManga(track: Track): Observable<Track?> {
|
suspend fun statusLibManga(track: Track): Track? {
|
||||||
val urlUserRead = "$apiUrl/collection/${track.media_id}"
|
val urlUserRead = "$apiUrl/collection/${track.media_id}"
|
||||||
val requestUserRead = Request.Builder()
|
val requestUserRead = Request.Builder()
|
||||||
.url(urlUserRead)
|
.url(urlUserRead)
|
||||||
@ -144,28 +136,27 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
|
|||||||
.build()
|
.build()
|
||||||
|
|
||||||
// todo get user readed chapter here
|
// todo get user readed chapter here
|
||||||
return authClient.newCall(requestUserRead)
|
val response = authClient.newCall(requestUserRead).await()
|
||||||
.asObservableSuccess()
|
val resp = response.body?.toString()
|
||||||
.map { netResponse ->
|
|
||||||
val resp = netResponse.body?.string()
|
|
||||||
val coll = gson.fromJson(resp, Collection::class.java)
|
val coll = gson.fromJson(resp, Collection::class.java)
|
||||||
track.status = coll.status?.id!!
|
track.status = coll.status?.id!!
|
||||||
track.last_chapter_read = coll.ep_status!!
|
track.last_chapter_read = coll.ep_status!!
|
||||||
track
|
return track
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun accessToken(code: String): Observable<OAuth> {
|
suspend fun accessToken(code: String): OAuth {
|
||||||
return client.newCall(accessTokenRequest(code)).asObservableSuccess().map { netResponse ->
|
return withContext(Dispatchers.IO){
|
||||||
|
val netResponse = client.newCall(accessTokenRequest(code)).execute()
|
||||||
val responseBody = netResponse.body?.string().orEmpty()
|
val responseBody = netResponse.body?.string().orEmpty()
|
||||||
if (responseBody.isEmpty()) {
|
if(responseBody.isEmpty()){
|
||||||
throw Exception("Null Response")
|
throw Exception("Null Response")
|
||||||
}
|
}
|
||||||
gson.fromJson(responseBody, OAuth::class.java)
|
gson.fromJson(responseBody, OAuth::class.java)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun accessTokenRequest(code: String) = POST(oauthUrl,
|
private fun accessTokenRequest(code: String) = POST(
|
||||||
|
oauthUrl,
|
||||||
body = FormBody.Builder()
|
body = FormBody.Builder()
|
||||||
.add("grant_type", "authorization_code")
|
.add("grant_type", "authorization_code")
|
||||||
.add("client_id", clientId)
|
.add("client_id", clientId)
|
||||||
@ -198,14 +189,15 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
|
|||||||
.appendQueryParameter("redirect_uri", redirectUrl)
|
.appendQueryParameter("redirect_uri", redirectUrl)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
fun refreshTokenRequest(token: String) = POST(oauthUrl,
|
fun refreshTokenRequest(token: String) = POST(
|
||||||
|
oauthUrl,
|
||||||
body = FormBody.Builder()
|
body = FormBody.Builder()
|
||||||
.add("grant_type", "refresh_token")
|
.add("grant_type", "refresh_token")
|
||||||
.add("client_id", clientId)
|
.add("client_id", clientId)
|
||||||
.add("client_secret", clientSecret)
|
.add("client_secret", clientSecret)
|
||||||
.add("refresh_token", token)
|
.add("refresh_token", token)
|
||||||
.add("redirect_uri", redirectUrl)
|
.add("redirect_uri", redirectUrl)
|
||||||
.build())
|
.build()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -47,8 +47,8 @@ class BangumiInterceptor(val bangumi: Bangumi, val gson: Gson) : Interceptor {
|
|||||||
return chain.proceed(authRequest)
|
return chain.proceed(authRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun newAuth(oauth: OAuth?) {
|
fun newAuth(oauth: OAuth) {
|
||||||
this.oauth = if (oauth == null) null else OAuth(
|
this.oauth = OAuth(
|
||||||
oauth.access_token,
|
oauth.access_token,
|
||||||
oauth.token_type,
|
oauth.token_type,
|
||||||
System.currentTimeMillis() / 1000,
|
System.currentTimeMillis() / 1000,
|
||||||
@ -58,4 +58,8 @@ class BangumiInterceptor(val bangumi: Bangumi, val gson: Gson) : Interceptor {
|
|||||||
|
|
||||||
bangumi.saveToken(oauth)
|
bangumi.saveToken(oauth)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun clearOauth(){
|
||||||
|
bangumi.saveToken(null)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,3 +11,39 @@ data class Collection(
|
|||||||
val user: User? = User(),
|
val user: User? = User(),
|
||||||
val vol_status: Int? = 0
|
val vol_status: Int? = 0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class OAuth(
|
||||||
|
val access_token: String,
|
||||||
|
val token_type: String,
|
||||||
|
val created_at: Long,
|
||||||
|
val expires_in: Long,
|
||||||
|
val refresh_token: String?,
|
||||||
|
val user_id: Long?
|
||||||
|
) {
|
||||||
|
// Access token refresh before expired
|
||||||
|
fun isExpired() = (System.currentTimeMillis() / 1000) > (created_at + expires_in - 3600)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Status(
|
||||||
|
val id: Int? = 0,
|
||||||
|
val name: String? = "",
|
||||||
|
val type: String? = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
data class User(
|
||||||
|
val avatar: Avatar? = Avatar(),
|
||||||
|
val id: Int? = 0,
|
||||||
|
val nickname: String? = "",
|
||||||
|
val sign: String? = "",
|
||||||
|
val url: String? = "",
|
||||||
|
val usergroup: Int? = 0,
|
||||||
|
val username: String? = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Avatar(
|
||||||
|
val large: String? = "",
|
||||||
|
val medium: String? = "",
|
||||||
|
val small: String? = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,16 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.bangumi
|
|
||||||
|
|
||||||
data class OAuth(
|
|
||||||
val access_token: String,
|
|
||||||
val token_type: String,
|
|
||||||
val created_at: Long,
|
|
||||||
val expires_in: Long,
|
|
||||||
val refresh_token: String?,
|
|
||||||
val user_id: Long?
|
|
||||||
) {
|
|
||||||
|
|
||||||
// Access token refresh before expired
|
|
||||||
fun isExpired() = (System.currentTimeMillis() / 1000) > (created_at + expires_in - 3600)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.bangumi
|
|
||||||
|
|
||||||
data class Status(
|
|
||||||
val id: Int? = 0,
|
|
||||||
val name: String? = "",
|
|
||||||
val type: String? = ""
|
|
||||||
)
|
|
@ -1,11 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.bangumi
|
|
||||||
|
|
||||||
data class User(
|
|
||||||
val avatar: Avatar? = Avatar(),
|
|
||||||
val id: Int? = 0,
|
|
||||||
val nickname: String? = "",
|
|
||||||
val sign: String? = "",
|
|
||||||
val url: String? = "",
|
|
||||||
val usergroup: Int? = 0,
|
|
||||||
val username: String? = ""
|
|
||||||
)
|
|
@ -7,8 +7,7 @@ import eu.kanade.tachiyomi.R
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import rx.Completable
|
import timber.log.Timber
|
||||||
import rx.Observable
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.text.DecimalFormat
|
import java.text.DecimalFormat
|
||||||
|
|
||||||
@ -70,11 +69,11 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
return df.format(track.score)
|
return df.format(track.score)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun add(track: Track): Observable<Track> {
|
override suspend fun add(track: Track): Track {
|
||||||
return api.addLibManga(track, getUserId())
|
return api.addLibManga(track, getUserId())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun update(track: Track): Observable<Track> {
|
override suspend fun update(track: Track): Track {
|
||||||
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
|
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
|
||||||
track.status = COMPLETED
|
track.status = COMPLETED
|
||||||
}
|
}
|
||||||
@ -82,41 +81,41 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
return api.updateLibManga(track)
|
return api.updateLibManga(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun bind(track: Track): Observable<Track> {
|
override suspend fun bind(track: Track): Track {
|
||||||
return api.findLibManga(track, getUserId())
|
val remoteTrack = api.findLibManga(track, getUserId())
|
||||||
.flatMap { remoteTrack ->
|
|
||||||
if (remoteTrack != null) {
|
if (remoteTrack != null) {
|
||||||
track.copyPersonalFrom(remoteTrack)
|
track.copyPersonalFrom(remoteTrack)
|
||||||
track.media_id = remoteTrack.media_id
|
track.media_id = remoteTrack.media_id
|
||||||
update(track)
|
return update(track)
|
||||||
} else {
|
} else {
|
||||||
track.score = DEFAULT_SCORE
|
track.score = DEFAULT_SCORE
|
||||||
track.status = DEFAULT_STATUS
|
track.status = DEFAULT_STATUS
|
||||||
add(track)
|
return add(track)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun search(query: String): Observable<List<TrackSearch>> {
|
override suspend fun search(query: String): List<TrackSearch> {
|
||||||
return api.search(query)
|
return api.search(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun refresh(track: Track): Observable<Track> {
|
override suspend fun refresh(track: Track): Track {
|
||||||
return api.getLibManga(track)
|
val remoteTrack = api.getLibManga(track)
|
||||||
.map { remoteTrack ->
|
|
||||||
track.copyPersonalFrom(remoteTrack)
|
track.copyPersonalFrom(remoteTrack)
|
||||||
track.total_chapters = remoteTrack.total_chapters
|
track.total_chapters = remoteTrack.total_chapters
|
||||||
track
|
return track
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun login(username: String, password: String): Completable {
|
override suspend fun login(username: String, password: String): Boolean {
|
||||||
return api.login(username, password)
|
try {
|
||||||
.doOnNext { interceptor.newAuth(it) }
|
val oauth = api.login(username, password)
|
||||||
.flatMap { api.getCurrentUser() }
|
interceptor.newAuth(oauth)
|
||||||
.doOnNext { userId -> saveCredentials(username, userId) }
|
val userId = api.getCurrentUser()
|
||||||
.doOnError { logout() }
|
saveCredentials(username, userId)
|
||||||
.toCompletable()
|
return true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun logout() {
|
override fun logout() {
|
||||||
@ -140,5 +139,4 @@ class Kitsu(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
package eu.kanade.tachiyomi.data.track.kitsu
|
package eu.kanade.tachiyomi.data.track.kitsu
|
||||||
|
|
||||||
import com.github.salomonbrys.kotson.*
|
import com.github.salomonbrys.kotson.array
|
||||||
|
import com.github.salomonbrys.kotson.get
|
||||||
|
import com.github.salomonbrys.kotson.int
|
||||||
|
import com.github.salomonbrys.kotson.jsonObject
|
||||||
|
import com.github.salomonbrys.kotson.obj
|
||||||
|
import com.github.salomonbrys.kotson.string
|
||||||
import com.google.gson.GsonBuilder
|
import com.google.gson.GsonBuilder
|
||||||
import com.google.gson.JsonObject
|
import com.google.gson.JsonObject
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
@ -11,8 +16,16 @@ import okhttp3.OkHttpClient
|
|||||||
import retrofit2.Retrofit
|
import retrofit2.Retrofit
|
||||||
import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory
|
import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory
|
||||||
import retrofit2.converter.gson.GsonConverterFactory
|
import retrofit2.converter.gson.GsonConverterFactory
|
||||||
import retrofit2.http.*
|
import retrofit2.http.Body
|
||||||
import rx.Observable
|
import retrofit2.http.Field
|
||||||
|
import retrofit2.http.FormUrlEncoded
|
||||||
|
import retrofit2.http.GET
|
||||||
|
import retrofit2.http.Header
|
||||||
|
import retrofit2.http.Headers
|
||||||
|
import retrofit2.http.PATCH
|
||||||
|
import retrofit2.http.POST
|
||||||
|
import retrofit2.http.Path
|
||||||
|
import retrofit2.http.Query
|
||||||
|
|
||||||
class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) {
|
class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor) {
|
||||||
|
|
||||||
@ -42,8 +55,7 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
|||||||
.build()
|
.build()
|
||||||
.create(KitsuApi.AgoliaSearchRest::class.java)
|
.create(KitsuApi.AgoliaSearchRest::class.java)
|
||||||
|
|
||||||
fun addLibManga(track: Track, userId: String): Observable<Track> {
|
suspend fun addLibManga(track: Track, userId: String): Track {
|
||||||
return Observable.defer {
|
|
||||||
// @formatter:off
|
// @formatter:off
|
||||||
val data = jsonObject(
|
val data = jsonObject(
|
||||||
"type" to "libraryEntries",
|
"type" to "libraryEntries",
|
||||||
@ -67,16 +79,12 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
rest.addLibManga(jsonObject("data" to data))
|
val json = rest.addLibManga(jsonObject("data" to data))
|
||||||
.map { json ->
|
|
||||||
track.media_id = json["data"]["id"].int
|
track.media_id = json["data"]["id"].int
|
||||||
track
|
return track
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateLibManga(track: Track): Observable<Track> {
|
suspend fun updateLibManga(track: Track): Track {
|
||||||
return Observable.defer {
|
|
||||||
// @formatter:off
|
// @formatter:off
|
||||||
val data = jsonObject(
|
val data = jsonObject(
|
||||||
"type" to "libraryEntries",
|
"type" to "libraryEntries",
|
||||||
@ -90,60 +98,46 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
|||||||
// @formatter:on
|
// @formatter:on
|
||||||
|
|
||||||
rest.updateLibManga(track.media_id, jsonObject("data" to data))
|
rest.updateLibManga(track.media_id, jsonObject("data" to data))
|
||||||
.map { track }
|
return track
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun search(query: String): List<TrackSearch> {
|
||||||
fun search(query: String): Observable<List<TrackSearch>> {
|
val key = searchRest.getKey()["media"].asJsonObject["key"].string
|
||||||
return searchRest
|
return algoliaSearch(key, query)
|
||||||
.getKey().map { json ->
|
|
||||||
json["media"].asJsonObject["key"].string
|
|
||||||
}.flatMap { key ->
|
|
||||||
algoliaSearch(key, query)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun algoliaSearch(key: String, query: String): List<TrackSearch> {
|
||||||
private fun algoliaSearch(key: String, query: String): Observable<List<TrackSearch>> {
|
|
||||||
val jsonObject = jsonObject("params" to "query=$query$algoliaFilter")
|
val jsonObject = jsonObject("params" to "query=$query$algoliaFilter")
|
||||||
return algoliaRest
|
val json = algoliaRest.getSearchQuery(algoliaAppId, key, jsonObject)
|
||||||
.getSearchQuery(algoliaAppId, key, jsonObject)
|
|
||||||
.map { json ->
|
|
||||||
val data = json["hits"].array
|
val data = json["hits"].array
|
||||||
data.map { KitsuSearchManga(it.obj) }
|
return data.map { KitsuSearchManga(it.obj) }
|
||||||
.filter { it.subType != "novel" }
|
.filter { it.subType != "novel" }
|
||||||
.map { it.toTrack() }
|
.map { it.toTrack() }
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun findLibManga(track: Track, userId: String): Observable<Track?> {
|
suspend fun findLibManga(track: Track, userId: String): Track? {
|
||||||
return rest.findLibManga(track.media_id, userId)
|
val json = rest.findLibManga(track.media_id, userId)
|
||||||
.map { json ->
|
|
||||||
val data = json["data"].array
|
val data = json["data"].array
|
||||||
if (data.size() > 0) {
|
return if (data.size() > 0) {
|
||||||
val manga = json["included"].array[0].obj
|
val manga = json["included"].array[0].obj
|
||||||
KitsuLibManga(data[0].obj, manga).toTrack()
|
KitsuLibManga(data[0].obj, manga).toTrack()
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun getLibManga(track: Track): Observable<Track> {
|
suspend fun getLibManga(track: Track): Track {
|
||||||
return rest.getLibManga(track.media_id)
|
val json = rest.getLibManga(track.media_id)
|
||||||
.map { json ->
|
|
||||||
val data = json["data"].array
|
val data = json["data"].array
|
||||||
if (data.size() > 0) {
|
if (data.size() > 0) {
|
||||||
val manga = json["included"].array[0].obj
|
val manga = json["included"].array[0].obj
|
||||||
KitsuLibManga(data[0].obj, manga).toTrack()
|
return KitsuLibManga(data[0].obj, manga).toTrack()
|
||||||
} else {
|
} else {
|
||||||
throw Exception("Could not find manga")
|
throw Exception("Could not find manga")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun login(username: String, password: String): Observable<OAuth> {
|
suspend fun login(username: String, password: String): OAuth {
|
||||||
return Retrofit.Builder()
|
return Retrofit.Builder()
|
||||||
.baseUrl(loginUrl)
|
.baseUrl(loginUrl)
|
||||||
.client(client)
|
.client(client)
|
||||||
@ -154,95 +148,99 @@ class KitsuApi(private val client: OkHttpClient, interceptor: KitsuInterceptor)
|
|||||||
.requestAccessToken(username, password)
|
.requestAccessToken(username, password)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCurrentUser(): Observable<String> {
|
suspend fun getCurrentUser(): String {
|
||||||
return rest.getCurrentUser().map { it["data"].array[0]["id"].string }
|
val currentUser = rest.getCurrentUser()
|
||||||
|
return currentUser["data"].array[0]["id"].string
|
||||||
}
|
}
|
||||||
|
|
||||||
private interface Rest {
|
private interface Rest {
|
||||||
|
|
||||||
@Headers("Content-Type: application/vnd.api+json")
|
@Headers("Content-Type: application/vnd.api+json")
|
||||||
@POST("library-entries")
|
@POST("library-entries")
|
||||||
fun addLibManga(
|
suspend fun addLibManga(
|
||||||
@Body data: JsonObject
|
@Body data: JsonObject
|
||||||
): Observable<JsonObject>
|
): JsonObject
|
||||||
|
|
||||||
@Headers("Content-Type: application/vnd.api+json")
|
@Headers("Content-Type: application/vnd.api+json")
|
||||||
@PATCH("library-entries/{id}")
|
@PATCH("library-entries/{id}")
|
||||||
fun updateLibManga(
|
suspend fun updateLibManga(
|
||||||
@Path("id") remoteId: Int,
|
@Path("id") remoteId: Int,
|
||||||
@Body data: JsonObject
|
@Body data: JsonObject
|
||||||
): Observable<JsonObject>
|
): JsonObject
|
||||||
|
|
||||||
|
|
||||||
@GET("library-entries")
|
@GET("library-entries")
|
||||||
fun findLibManga(
|
suspend fun findLibManga(
|
||||||
@Query("filter[manga_id]", encoded = true) remoteId: Int,
|
@Query("filter[manga_id]", encoded = true) remoteId: Int,
|
||||||
@Query("filter[user_id]", encoded = true) userId: String,
|
@Query("filter[user_id]", encoded = true) userId: String,
|
||||||
@Query("include") includes: String = "manga"
|
@Query("include") includes: String = "manga"
|
||||||
): Observable<JsonObject>
|
): JsonObject
|
||||||
|
|
||||||
@GET("library-entries")
|
@GET("library-entries")
|
||||||
fun getLibManga(
|
suspend fun getLibManga(
|
||||||
@Query("filter[id]", encoded = true) remoteId: Int,
|
@Query("filter[id]", encoded = true) remoteId: Int,
|
||||||
@Query("include") includes: String = "manga"
|
@Query("include") includes: String = "manga"
|
||||||
): Observable<JsonObject>
|
): JsonObject
|
||||||
|
|
||||||
@GET("users")
|
@GET("users")
|
||||||
fun getCurrentUser(
|
suspend fun getCurrentUser(
|
||||||
@Query("filter[self]", encoded = true) self: Boolean = true
|
@Query("filter[self]", encoded = true) self: Boolean = true
|
||||||
): Observable<JsonObject>
|
): JsonObject
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private interface SearchKeyRest {
|
private interface SearchKeyRest {
|
||||||
@GET("media/")
|
@GET("media/")
|
||||||
fun getKey(): Observable<JsonObject>
|
suspend fun getKey(): JsonObject
|
||||||
}
|
}
|
||||||
|
|
||||||
private interface AgoliaSearchRest {
|
private interface AgoliaSearchRest {
|
||||||
@POST("query/")
|
@POST("query/")
|
||||||
fun getSearchQuery(@Header("X-Algolia-Application-Id") appid: String, @Header("X-Algolia-API-Key") key: String, @Body json: JsonObject): Observable<JsonObject>
|
suspend fun getSearchQuery(
|
||||||
|
@Header("X-Algolia-Application-Id") appid: String,
|
||||||
|
@Header("X-Algolia-API-Key") key: String,
|
||||||
|
@Body json: JsonObject
|
||||||
|
): JsonObject
|
||||||
}
|
}
|
||||||
|
|
||||||
private interface LoginRest {
|
private interface LoginRest {
|
||||||
|
|
||||||
@FormUrlEncoded
|
@FormUrlEncoded
|
||||||
@POST("oauth/token")
|
@POST("oauth/token")
|
||||||
fun requestAccessToken(
|
suspend fun requestAccessToken(
|
||||||
@Field("username") username: String,
|
@Field("username") username: String,
|
||||||
@Field("password") password: String,
|
@Field("password") password: String,
|
||||||
@Field("grant_type") grantType: String = "password",
|
@Field("grant_type") grantType: String = "password",
|
||||||
@Field("client_id") client_id: String = clientId,
|
@Field("client_id") client_id: String = clientId,
|
||||||
@Field("client_secret") client_secret: String = clientSecret
|
@Field("client_secret") client_secret: String = clientSecret
|
||||||
): Observable<OAuth>
|
): OAuth
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val clientId = "dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd"
|
private const val clientId =
|
||||||
private const val clientSecret = "54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151"
|
"dd031b32d2f56c990b1425efe6c42ad847e7fe3ab46bf1299f05ecd856bdb7dd"
|
||||||
|
private const val clientSecret =
|
||||||
|
"54d7307928f63414defd96399fc31ba847961ceaecef3a5fd93144e960c0e151"
|
||||||
private const val baseUrl = "https://kitsu.io/api/edge/"
|
private const val baseUrl = "https://kitsu.io/api/edge/"
|
||||||
private const val loginUrl = "https://kitsu.io/api/"
|
private const val loginUrl = "https://kitsu.io/api/"
|
||||||
private const val baseMangaUrl = "https://kitsu.io/manga/"
|
private const val baseMangaUrl = "https://kitsu.io/manga/"
|
||||||
private const val algoliaKeyUrl = "https://kitsu.io/api/edge/algolia-keys/"
|
private const val algoliaKeyUrl = "https://kitsu.io/api/edge/algolia-keys/"
|
||||||
private const val algoliaUrl = "https://AWQO5J657S-dsn.algolia.net/1/indexes/production_media/"
|
private const val algoliaUrl =
|
||||||
|
"https://AWQO5J657S-dsn.algolia.net/1/indexes/production_media/"
|
||||||
private const val algoliaAppId = "AWQO5J657S"
|
private const val algoliaAppId = "AWQO5J657S"
|
||||||
private const val algoliaFilter = "&facetFilters=%5B%22kind%3Amanga%22%5D&attributesToRetrieve=%5B%22synopsis%22%2C%22canonicalTitle%22%2C%22chapterCount%22%2C%22posterImage%22%2C%22startDate%22%2C%22subtype%22%2C%22endDate%22%2C%20%22id%22%5D"
|
private const val algoliaFilter =
|
||||||
|
"&facetFilters=%5B%22kind%3Amanga%22%5D&attributesToRetrieve=%5B%22synopsis%22%2C%22canonicalTitle%22%2C%22chapterCount%22%2C%22posterImage%22%2C%22startDate%22%2C%22subtype%22%2C%22endDate%22%2C%20%22id%22%5D"
|
||||||
|
|
||||||
fun mangaUrl(remoteId: Int): String {
|
fun mangaUrl(remoteId: Int): String {
|
||||||
return baseMangaUrl + remoteId
|
return baseMangaUrl + remoteId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun refreshTokenRequest(token: String) = POST(
|
||||||
fun refreshTokenRequest(token: String) = POST("${loginUrl}oauth/token",
|
"${loginUrl}oauth/token",
|
||||||
body = FormBody.Builder()
|
body = FormBody.Builder()
|
||||||
.add("grant_type", "refresh_token")
|
.add("grant_type", "refresh_token")
|
||||||
.add("client_id", clientId)
|
.add("client_id", clientId)
|
||||||
.add("client_secret", clientSecret)
|
.add("client_secret", clientSecret)
|
||||||
.add("refresh_token", token)
|
.add("refresh_token", token)
|
||||||
.build())
|
.build()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -7,10 +7,7 @@ import eu.kanade.tachiyomi.data.database.models.Track
|
|||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import okhttp3.HttpUrl
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
import rx.Completable
|
|
||||||
import rx.Observable
|
|
||||||
|
|
||||||
class Myanimelist(private val context: Context, id: Int) : TrackService(id) {
|
class Myanimelist(private val context: Context, id: Int) : TrackService(id) {
|
||||||
|
|
||||||
@ -62,11 +59,11 @@ class Myanimelist(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
return track.score.toInt().toString()
|
return track.score.toInt().toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun add(track: Track): Observable<Track> {
|
override suspend fun add(track: Track): Track {
|
||||||
return api.addLibManga(track)
|
return api.addLibManga(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun update(track: Track): Observable<Track> {
|
override suspend fun update(track: Track): Track {
|
||||||
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
|
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
|
||||||
track.status = COMPLETED
|
track.status = COMPLETED
|
||||||
}
|
}
|
||||||
@ -74,9 +71,8 @@ class Myanimelist(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
return api.updateLibManga(track)
|
return api.updateLibManga(track)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun bind(track: Track): Observable<Track> {
|
override suspend fun bind(track: Track): Track {
|
||||||
return api.findLibManga(track)
|
val remoteTrack = api.findLibManga(track)
|
||||||
.flatMap { remoteTrack ->
|
|
||||||
if (remoteTrack != null) {
|
if (remoteTrack != null) {
|
||||||
track.copyPersonalFrom(remoteTrack)
|
track.copyPersonalFrom(remoteTrack)
|
||||||
update(track)
|
update(track)
|
||||||
@ -86,30 +82,31 @@ class Myanimelist(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
track.status = DEFAULT_STATUS
|
track.status = DEFAULT_STATUS
|
||||||
add(track)
|
add(track)
|
||||||
}
|
}
|
||||||
}
|
return track
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun search(query: String): Observable<List<TrackSearch>> {
|
override suspend fun search(query: String): List<TrackSearch> {
|
||||||
return api.search(query)
|
return api.search(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun refresh(track: Track): Observable<Track> {
|
override suspend fun refresh(track: Track): Track {
|
||||||
return api.getLibManga(track)
|
val remoteTrack = api.getLibManga(track)
|
||||||
.map { remoteTrack ->
|
|
||||||
track.copyPersonalFrom(remoteTrack)
|
track.copyPersonalFrom(remoteTrack)
|
||||||
track.total_chapters = remoteTrack.total_chapters
|
track.total_chapters = remoteTrack.total_chapters
|
||||||
track
|
return track
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun login(username: String, password: String): Completable {
|
override suspend fun login(username: String, password: String): Boolean {
|
||||||
logout()
|
logout()
|
||||||
|
try {
|
||||||
return Observable.fromCallable { api.login(username, password) }
|
val csrf = api.login(username, password)
|
||||||
.doOnNext { csrf -> saveCSRF(csrf) }
|
saveCSRF(csrf)
|
||||||
.doOnNext { saveCredentials(username, password) }
|
saveCredentials(username, password)
|
||||||
.doOnError { logout() }
|
return true
|
||||||
.toCompletable()
|
} catch (e: Exception) {
|
||||||
|
logout()
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun refreshLogin() {
|
fun refreshLogin() {
|
||||||
@ -160,5 +157,4 @@ class Myanimelist(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
|
|
||||||
return ckCount == 2
|
return ckCount == 2
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -6,8 +6,9 @@ import eu.kanade.tachiyomi.data.track.TrackManager
|
|||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.network.POST
|
||||||
import eu.kanade.tachiyomi.network.asObservable
|
import eu.kanade.tachiyomi.network.await
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
import eu.kanade.tachiyomi.network.consumeBody
|
||||||
|
import eu.kanade.tachiyomi.network.consumeXmlBody
|
||||||
import eu.kanade.tachiyomi.util.selectInt
|
import eu.kanade.tachiyomi.util.selectInt
|
||||||
import eu.kanade.tachiyomi.util.selectText
|
import eu.kanade.tachiyomi.util.selectText
|
||||||
import okhttp3.FormBody
|
import okhttp3.FormBody
|
||||||
@ -15,41 +16,29 @@ import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
|||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.RequestBody
|
import okhttp3.RequestBody
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import okhttp3.Response
|
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import org.jsoup.Jsoup
|
import org.jsoup.Jsoup
|
||||||
import org.jsoup.nodes.Document
|
import org.jsoup.nodes.Document
|
||||||
import org.jsoup.nodes.Element
|
import org.jsoup.nodes.Element
|
||||||
import org.jsoup.parser.Parser
|
import org.jsoup.parser.Parser
|
||||||
import rx.Observable
|
|
||||||
import java.io.BufferedReader
|
|
||||||
import java.io.InputStreamReader
|
|
||||||
import java.util.zip.GZIPInputStream
|
|
||||||
|
|
||||||
|
|
||||||
class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListInterceptor) {
|
class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListInterceptor) {
|
||||||
|
|
||||||
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
||||||
|
|
||||||
fun search(query: String): Observable<List<TrackSearch>> {
|
suspend fun search(query: String): List<TrackSearch> {
|
||||||
return if (query.startsWith(PREFIX_MY)) {
|
if (query.startsWith(PREFIX_MY)) {
|
||||||
val realQuery = query.removePrefix(PREFIX_MY)
|
val realQuery = query.removePrefix(PREFIX_MY)
|
||||||
getList()
|
return getList().filter { it.title.contains(realQuery, true) }.toList()
|
||||||
.flatMap { Observable.from(it) }
|
|
||||||
.filter { it.title.contains(realQuery, true) }
|
|
||||||
.toList()
|
|
||||||
} else {
|
} else {
|
||||||
client.newCall(GET(searchUrl(query)))
|
val realQuery = query.take(100)
|
||||||
.asObservable()
|
val response = client.newCall(GET(searchUrl(realQuery))).await()
|
||||||
.flatMap { response ->
|
val matches = Jsoup.parse(response.consumeBody())
|
||||||
Observable.from(Jsoup.parse(response.consumeBody())
|
|
||||||
.select("div.js-categories-seasonal.js-block-list.list")
|
.select("div.js-categories-seasonal.js-block-list.list")
|
||||||
.select("table").select("tbody")
|
.select("table").select("tbody")
|
||||||
.select("tr").drop(1))
|
.select("tr").drop(1)
|
||||||
}
|
|
||||||
.filter { row ->
|
return matches.filter { row -> row.select(TD)[2].text() != "Novel" }
|
||||||
row.select(TD)[2].text() != "Novel"
|
|
||||||
}
|
|
||||||
.map { row ->
|
.map { row ->
|
||||||
TrackSearch.create(TrackManager.MYANIMELIST).apply {
|
TrackSearch.create(TrackManager.MYANIMELIST).apply {
|
||||||
title = row.searchTitle()
|
title = row.searchTitle()
|
||||||
@ -67,46 +56,44 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addLibManga(track: Track): Observable<Track> {
|
suspend fun addLibManga(track: Track): Track {
|
||||||
return Observable.defer {
|
authClient.newCall(POST(url = addUrl(), body = mangaPostPayload(track))).await()
|
||||||
authClient.newCall(POST(url = addUrl(), body = mangaPostPayload(track)))
|
return track
|
||||||
.asObservableSuccess()
|
|
||||||
.map { track }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateLibManga(track: Track): Observable<Track> {
|
suspend fun updateLibManga(track: Track): Track {
|
||||||
return Observable.defer {
|
authClient.newCall(POST(url = updateUrl(), body = mangaPostPayload(track))).await()
|
||||||
authClient.newCall(POST(url = updateUrl(), body = mangaPostPayload(track)))
|
return track
|
||||||
.asObservableSuccess()
|
|
||||||
.map { track }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun findLibManga(track: Track): Observable<Track?> {
|
suspend fun findLibManga(track: Track): Track? {
|
||||||
return authClient.newCall(GET(url = listEntryUrl(track.media_id)))
|
val response = authClient.newCall(GET(url = listEntryUrl(track.media_id))).await()
|
||||||
.asObservable()
|
|
||||||
.map {response ->
|
|
||||||
var libTrack: Track? = null
|
var libTrack: Track? = null
|
||||||
response.use {
|
response.use {
|
||||||
if (it.priorResponse?.isRedirect != true) {
|
if (it.priorResponse?.isRedirect != true) {
|
||||||
val trackForm = Jsoup.parse(it.consumeBody())
|
val trackForm = Jsoup.parse(it.consumeBody())
|
||||||
|
|
||||||
libTrack = Track.create(TrackManager.MYANIMELIST).apply {
|
libTrack = Track.create(TrackManager.MYANIMELIST).apply {
|
||||||
last_chapter_read = trackForm.select("#add_manga_num_read_chapters").`val`().toInt()
|
last_chapter_read =
|
||||||
|
trackForm.select("#add_manga_num_read_chapters").`val`().toInt()
|
||||||
total_chapters = trackForm.select("#totalChap").text().toInt()
|
total_chapters = trackForm.select("#totalChap").text().toInt()
|
||||||
status = trackForm.select("#add_manga_status > option[selected]").`val`().toInt()
|
status =
|
||||||
score = trackForm.select("#add_manga_score > option[selected]").`val`().toFloatOrNull() ?: 0f
|
trackForm.select("#add_manga_status > option[selected]").`val`().toInt()
|
||||||
|
score = trackForm.select("#add_manga_score > option[selected]").`val`()
|
||||||
|
.toFloatOrNull() ?: 0f
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
libTrack
|
return libTrack
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getLibManga(track: Track): Observable<Track> {
|
suspend fun getLibManga(track: Track): Track {
|
||||||
return findLibManga(track)
|
val result = findLibManga(track)
|
||||||
.map { it ?: throw Exception("Could not find manga") }
|
if (result == null) {
|
||||||
|
throw Exception("Could not find manga")
|
||||||
|
} else {
|
||||||
|
return result
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun login(username: String, password: String): String {
|
fun login(username: String, password: String): String {
|
||||||
@ -126,22 +113,19 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun login(username: String, password: String, csrf: String) {
|
private fun login(username: String, password: String, csrf: String) {
|
||||||
val response = client.newCall(POST(url = loginUrl(), body = loginPostBody(username, password, csrf))).execute()
|
val response =
|
||||||
|
client.newCall(POST(url = loginUrl(), body = loginPostBody(username, password, csrf)))
|
||||||
|
.execute()
|
||||||
|
|
||||||
response.use {
|
response.use {
|
||||||
if (response.priorResponse?.code != 302) throw Exception("Authentication error")
|
if (response.priorResponse?.code != 302) throw Exception("Authentication error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getList(): Observable<List<TrackSearch>> {
|
private suspend fun getList(): List<TrackSearch> {
|
||||||
return getListUrl()
|
val results = getListXml(getListUrl()).select("manga")
|
||||||
.flatMap { url ->
|
|
||||||
getListXml(url)
|
return results.map {
|
||||||
}
|
|
||||||
.flatMap { doc ->
|
|
||||||
Observable.from(doc.select("manga"))
|
|
||||||
}
|
|
||||||
.map {
|
|
||||||
TrackSearch.create(TrackManager.MYANIMELIST).apply {
|
TrackSearch.create(TrackManager.MYANIMELIST).apply {
|
||||||
title = it.selectText("manga_title")!!
|
title = it.selectText("manga_title")!!
|
||||||
media_id = it.selectInt("manga_mangadb_id")
|
media_id = it.selectInt("manga_mangadb_id")
|
||||||
@ -155,43 +139,19 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
|
|||||||
.toList()
|
.toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getListUrl(): Observable<String> {
|
private suspend fun getListUrl(): String {
|
||||||
return authClient.newCall(POST(url = exportListUrl(), body = exportPostBody()))
|
val response =
|
||||||
.asObservable()
|
authClient.newCall(POST(url = exportListUrl(), body = exportPostBody())).await()
|
||||||
.map {response ->
|
|
||||||
baseUrl + Jsoup.parse(response.consumeBody())
|
return baseUrl + Jsoup.parse(response.consumeBody())
|
||||||
.select("div.goodresult")
|
.select("div.goodresult")
|
||||||
.select("a")
|
.select("a")
|
||||||
.attr("href")
|
.attr("href")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun getListXml(url: String): Observable<Document> {
|
private suspend fun getListXml(url: String): Document {
|
||||||
return authClient.newCall(GET(url))
|
val response = authClient.newCall(GET(url)).await()
|
||||||
.asObservable()
|
return Jsoup.parse(response.consumeXmlBody(), "", Parser.xmlParser())
|
||||||
.map { response ->
|
|
||||||
Jsoup.parse(response.consumeXmlBody(), "", Parser.xmlParser())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Response.consumeBody(): String? {
|
|
||||||
use {
|
|
||||||
if (it.code != 200) throw Exception("HTTP error ${it.code}")
|
|
||||||
return it.body?.string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Response.consumeXmlBody(): String? {
|
|
||||||
use { res ->
|
|
||||||
if (res.code != 200) throw Exception("Export list error")
|
|
||||||
BufferedReader(InputStreamReader(GZIPInputStream(res.body?.source()?.inputStream()))).use { reader ->
|
|
||||||
val sb = StringBuilder()
|
|
||||||
reader.forEachLine { line ->
|
|
||||||
sb.append(line)
|
|
||||||
}
|
|
||||||
return sb.toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@ -233,7 +193,7 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
|
|||||||
.toString()
|
.toString()
|
||||||
|
|
||||||
private fun addUrl() = Uri.parse(baseModifyListUrl).buildUpon()
|
private fun addUrl() = Uri.parse(baseModifyListUrl).buildUpon()
|
||||||
.appendPath( "add.json")
|
.appendPath("add.json")
|
||||||
.toString()
|
.toString()
|
||||||
|
|
||||||
private fun listEntryUrl(mediaId: Int) = Uri.parse(baseModifyListUrl).buildUpon()
|
private fun listEntryUrl(mediaId: Int) = Uri.parse(baseModifyListUrl).buildUpon()
|
||||||
@ -266,12 +226,14 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
|
|||||||
.put("score", track.score)
|
.put("score", track.score)
|
||||||
.put("num_read_chapters", track.last_chapter_read)
|
.put("num_read_chapters", track.last_chapter_read)
|
||||||
|
|
||||||
return body.toString().toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull())
|
return body.toString()
|
||||||
|
.toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Element.searchTitle() = select("strong").text()!!
|
private fun Element.searchTitle() = select("strong").text()!!
|
||||||
|
|
||||||
private fun Element.searchTotalChapters() = if (select(TD)[4].text() == "-") 0 else select(TD)[4].text().toInt()
|
private fun Element.searchTotalChapters() =
|
||||||
|
if (select(TD)[4].text() == "-") 0 else select(TD)[4].text().toInt()
|
||||||
|
|
||||||
private fun Element.searchCoverUrl() = select("img")
|
private fun Element.searchCoverUrl() = select("img")
|
||||||
.attr("data-src")
|
.attr("data-src")
|
||||||
@ -287,7 +249,8 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
|
|||||||
.first()
|
.first()
|
||||||
.ownText()!!
|
.ownText()!!
|
||||||
|
|
||||||
private fun Element.searchPublishingStatus() = if (select(TD).last().text() == "-") "Publishing" else "Finished"
|
private fun Element.searchPublishingStatus() =
|
||||||
|
if (select(TD).last().text() == "-") "Publishing" else "Finished"
|
||||||
|
|
||||||
private fun Element.searchPublishingType() = select(TD)[2].text()!!
|
private fun Element.searchPublishingType() = select(TD)[2].text()!!
|
||||||
|
|
||||||
|
@ -7,8 +7,7 @@ import eu.kanade.tachiyomi.R
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import rx.Completable
|
import timber.log.Timber
|
||||||
import rx.Observable
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class Shikimori(private val context: Context, id: Int) : TrackService(id) {
|
class Shikimori(private val context: Context, id: Int) : TrackService(id) {
|
||||||
@ -21,20 +20,20 @@ class Shikimori(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
return track.score.toInt().toString()
|
return track.score.toInt().toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun add(track: Track): Observable<Track> {
|
override suspend fun add(track: Track): Track {
|
||||||
return api.addLibManga(track, getUsername())
|
return api.addLibManga(track, getUsername())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun update(track: Track): Observable<Track> {
|
override suspend fun update(track: Track): Track {
|
||||||
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
|
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
|
||||||
track.status = COMPLETED
|
track.status = COMPLETED
|
||||||
}
|
}
|
||||||
return api.updateLibManga(track, getUsername())
|
return api.updateLibManga(track, getUsername())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun bind(track: Track): Observable<Track> {
|
override suspend fun bind(track: Track): Track {
|
||||||
return api.findLibManga(track, getUsername())
|
val remoteTrack = api.findLibManga(track, getUsername())
|
||||||
.flatMap { remoteTrack ->
|
|
||||||
if (remoteTrack != null) {
|
if (remoteTrack != null) {
|
||||||
track.copyPersonalFrom(remoteTrack)
|
track.copyPersonalFrom(remoteTrack)
|
||||||
track.library_id = remoteTrack.library_id
|
track.library_id = remoteTrack.library_id
|
||||||
@ -45,22 +44,21 @@ class Shikimori(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
track.status = DEFAULT_STATUS
|
track.status = DEFAULT_STATUS
|
||||||
add(track)
|
add(track)
|
||||||
}
|
}
|
||||||
}
|
return track
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun search(query: String): Observable<List<TrackSearch>> {
|
override suspend fun search(query: String): List<TrackSearch> {
|
||||||
return api.search(query)
|
return api.search(query)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun refresh(track: Track): Observable<Track> {
|
override suspend fun refresh(track: Track): Track {
|
||||||
return api.findLibManga(track, getUsername())
|
val remoteTrack = api.findLibManga(track, getUsername())
|
||||||
.map { remoteTrack ->
|
|
||||||
if (remoteTrack != null) {
|
if (remoteTrack != null) {
|
||||||
track.copyPersonalFrom(remoteTrack)
|
track.copyPersonalFrom(remoteTrack)
|
||||||
track.total_chapters = remoteTrack.total_chapters
|
track.total_chapters = remoteTrack.total_chapters
|
||||||
}
|
}
|
||||||
track
|
return track
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@ -103,18 +101,21 @@ class Shikimori(private val context: Context, id: Int) : TrackService(id) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun login(username: String, password: String) = login(password)
|
override suspend fun login(username: String, password: String) = login(password)
|
||||||
|
|
||||||
|
suspend fun login(code: String): Boolean {
|
||||||
|
try {
|
||||||
|
val oauth = api.accessToken(code)
|
||||||
|
|
||||||
fun login(code: String): Completable {
|
|
||||||
return api.accessToken(code).map { oauth: OAuth? ->
|
|
||||||
interceptor.newAuth(oauth)
|
interceptor.newAuth(oauth)
|
||||||
if (oauth != null) {
|
|
||||||
val user = api.getCurrentUser()
|
val user = api.getCurrentUser()
|
||||||
saveCredentials(user.toString(), oauth.access_token)
|
saveCredentials(user.toString(), oauth.access_token)
|
||||||
}
|
return true
|
||||||
}.doOnError {
|
} catch (e: java.lang.Exception) {
|
||||||
|
Timber.e(e)
|
||||||
logout()
|
logout()
|
||||||
}.toCompletable()
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveToken(oauth: OAuth?) {
|
fun saveToken(oauth: OAuth?) {
|
||||||
|
@ -14,23 +14,25 @@ import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
|||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.network.POST
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
import okhttp3.*
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import okhttp3.FormBody
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.RequestBody
|
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import rx.Observable
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInterceptor) {
|
class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInterceptor) {
|
||||||
|
|
||||||
private val gson: Gson by injectLazy()
|
private val gson: Gson by injectLazy()
|
||||||
private val parser = JsonParser()
|
|
||||||
private val jsonime = "application/json; charset=utf-8".toMediaTypeOrNull()
|
private val jsonime = "application/json; charset=utf-8".toMediaTypeOrNull()
|
||||||
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
|
||||||
|
|
||||||
fun addLibManga(track: Track, user_id: String): Observable<Track> {
|
suspend fun updateLibManga(track: Track, user_id: String): Track = addLibManga(track, user_id)
|
||||||
|
|
||||||
|
suspend fun addLibManga(track: Track, user_id: String): Track {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
val payload = jsonObject(
|
val payload = jsonObject(
|
||||||
"user_rate" to jsonObject(
|
"user_rate" to jsonObject(
|
||||||
"user_id" to user_id,
|
"user_id" to user_id,
|
||||||
@ -46,16 +48,13 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
|
|||||||
.url("$apiUrl/v2/user_rates")
|
.url("$apiUrl/v2/user_rates")
|
||||||
.post(body)
|
.post(body)
|
||||||
.build()
|
.build()
|
||||||
return authClient.newCall(request)
|
authClient.newCall(request).execute()
|
||||||
.asObservableSuccess()
|
|
||||||
.map {
|
|
||||||
track
|
track
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun updateLibManga(track: Track, user_id: String): Observable<Track> = addLibManga(track, user_id)
|
suspend fun search(search: String): List<TrackSearch> {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
fun search(search: String): Observable<List<TrackSearch>> {
|
|
||||||
val url = Uri.parse("$apiUrl/mangas").buildUpon()
|
val url = Uri.parse("$apiUrl/mangas").buildUpon()
|
||||||
.appendQueryParameter("order", "popularity")
|
.appendQueryParameter("order", "popularity")
|
||||||
.appendQueryParameter("search", search)
|
.appendQueryParameter("search", search)
|
||||||
@ -65,17 +64,17 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
|
|||||||
.url(url.toString())
|
.url(url.toString())
|
||||||
.get()
|
.get()
|
||||||
.build()
|
.build()
|
||||||
return authClient.newCall(request)
|
val netResponse = authClient.newCall(request).execute()
|
||||||
.asObservableSuccess()
|
|
||||||
.map { netResponse ->
|
|
||||||
val responseBody = netResponse.body?.string().orEmpty()
|
val responseBody = netResponse.body?.string().orEmpty()
|
||||||
if (responseBody.isEmpty()) {
|
if (responseBody.isEmpty()) {
|
||||||
throw Exception("Null Response")
|
throw Exception("Null Response")
|
||||||
}
|
}
|
||||||
val response = parser.parse(responseBody).array
|
val response = JsonParser.parseString(responseBody).array
|
||||||
response.map { jsonToSearch(it.obj) }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
response.map { jsonToSearch(it.obj) }
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun jsonToSearch(obj: JsonObject): TrackSearch {
|
private fun jsonToSearch(obj: JsonObject): TrackSearch {
|
||||||
@ -104,7 +103,8 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun findLibManga(track: Track, user_id: String): Observable<Track?> {
|
suspend fun findLibManga(track: Track, user_id: String): Track? {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
val url = Uri.parse("$apiUrl/v2/user_rates").buildUpon()
|
val url = Uri.parse("$apiUrl/v2/user_rates").buildUpon()
|
||||||
.appendQueryParameter("user_id", user_id)
|
.appendQueryParameter("user_id", user_id)
|
||||||
.appendQueryParameter("target_id", track.media_id.toString())
|
.appendQueryParameter("target_id", track.media_id.toString())
|
||||||
@ -122,20 +122,18 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
|
|||||||
.url(urlMangas.toString())
|
.url(urlMangas.toString())
|
||||||
.get()
|
.get()
|
||||||
.build()
|
.build()
|
||||||
return authClient.newCall(requestMangas)
|
|
||||||
.asObservableSuccess()
|
val requestMangasResponse = authClient.newCall(requestMangas).execute()
|
||||||
.map { netResponse ->
|
val requestMangasBody = requestMangasResponse.body?.string().orEmpty()
|
||||||
val responseBody = netResponse.body?.string().orEmpty()
|
val mangas = JsonParser.parseString(requestMangasBody).obj
|
||||||
parser.parse(responseBody).obj
|
|
||||||
}.flatMap { mangas ->
|
val requestResponse = authClient.newCall(request).execute()
|
||||||
authClient.newCall(request)
|
val requestResponseBody = requestResponse.body?.string().orEmpty()
|
||||||
.asObservableSuccess()
|
|
||||||
.map { netResponse ->
|
if (requestResponseBody.isEmpty()) {
|
||||||
val responseBody = netResponse.body?.string().orEmpty()
|
|
||||||
if (responseBody.isEmpty()) {
|
|
||||||
throw Exception("Null Response")
|
throw Exception("Null Response")
|
||||||
}
|
}
|
||||||
val response = parser.parse(responseBody).array
|
val response = JsonParser.parseString(requestResponseBody).array
|
||||||
if (response.size() > 1) {
|
if (response.size() > 1) {
|
||||||
throw Exception("Too much mangas in response")
|
throw Exception("Too much mangas in response")
|
||||||
}
|
}
|
||||||
@ -145,15 +143,15 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
|
|||||||
entry.firstOrNull()
|
entry.firstOrNull()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun getCurrentUser(): Int {
|
fun getCurrentUser(): Int {
|
||||||
val user = authClient.newCall(GET("$apiUrl/users/whoami")).execute().body?.string()
|
val user = authClient.newCall(GET("$apiUrl/users/whoami")).execute().body?.string()
|
||||||
return parser.parse(user).obj["id"].asInt
|
return JsonParser.parseString(user).obj["id"].asInt
|
||||||
}
|
}
|
||||||
|
|
||||||
fun accessToken(code: String): Observable<OAuth> {
|
suspend fun accessToken(code: String): OAuth {
|
||||||
return client.newCall(accessTokenRequest(code)).asObservableSuccess().map { netResponse ->
|
return withContext(Dispatchers.IO) {
|
||||||
|
val netResponse= client.newCall(accessTokenRequest(code)).execute()
|
||||||
val responseBody = netResponse.body?.string().orEmpty()
|
val responseBody = netResponse.body?.string().orEmpty()
|
||||||
if (responseBody.isEmpty()) {
|
if (responseBody.isEmpty()) {
|
||||||
throw Exception("Null Response")
|
throw Exception("Null Response")
|
||||||
@ -162,7 +160,8 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun accessTokenRequest(code: String) = POST(oauthUrl,
|
private fun accessTokenRequest(code: String) = POST(
|
||||||
|
oauthUrl,
|
||||||
body = FormBody.Builder()
|
body = FormBody.Builder()
|
||||||
.add("grant_type", "authorization_code")
|
.add("grant_type", "authorization_code")
|
||||||
.add("client_id", clientId)
|
.add("client_id", clientId)
|
||||||
@ -172,10 +171,11 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
|
|||||||
.build()
|
.build()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val clientId = "1aaf4cf232372708e98b5abc813d795b539c5a916dbbfe9ac61bf02a360832cc"
|
private const val clientId =
|
||||||
private const val clientSecret = "229942c742dd4cde803125d17d64501d91c0b12e14cb1e5120184d77d67024c0"
|
"1aaf4cf232372708e98b5abc813d795b539c5a916dbbfe9ac61bf02a360832cc"
|
||||||
|
private const val clientSecret =
|
||||||
|
"229942c742dd4cde803125d17d64501d91c0b12e14cb1e5120184d77d67024c0"
|
||||||
|
|
||||||
private const val baseUrl = "https://shikimori.one"
|
private const val baseUrl = "https://shikimori.one"
|
||||||
private const val apiUrl = "https://shikimori.one/api"
|
private const val apiUrl = "https://shikimori.one/api"
|
||||||
@ -196,15 +196,14 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
|
|||||||
.appendQueryParameter("response_type", "code")
|
.appendQueryParameter("response_type", "code")
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
|
fun refreshTokenRequest(token: String) = POST(
|
||||||
fun refreshTokenRequest(token: String) = POST(oauthUrl,
|
oauthUrl,
|
||||||
body = FormBody.Builder()
|
body = FormBody.Builder()
|
||||||
.add("grant_type", "refresh_token")
|
.add("grant_type", "refresh_token")
|
||||||
.add("client_id", clientId)
|
.add("client_id", clientId)
|
||||||
.add("client_secret", clientSecret)
|
.add("client_secret", clientSecret)
|
||||||
.add("refresh_token", token)
|
.add("refresh_token", token)
|
||||||
.build())
|
.build()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,13 @@
|
|||||||
package eu.kanade.tachiyomi.data.updater
|
package eu.kanade.tachiyomi.data.updater
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import eu.kanade.tachiyomi.data.updater.devrepo.DevRepoUpdateChecker
|
|
||||||
import eu.kanade.tachiyomi.data.updater.github.GithubUpdateChecker
|
import eu.kanade.tachiyomi.data.updater.github.GithubUpdateChecker
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
|
||||||
abstract class UpdateChecker {
|
abstract class UpdateChecker {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun getUpdateChecker(): UpdateChecker {
|
fun getUpdateChecker(): UpdateChecker = GithubUpdateChecker()
|
||||||
return if (BuildConfig.DEBUG) {
|
|
||||||
DevRepoUpdateChecker()
|
|
||||||
} else {
|
|
||||||
GithubUpdateChecker()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -23,7 +23,7 @@ class ExtensionUpdateJob : Job() {
|
|||||||
|
|
||||||
override fun onRunJob(params: Params): Result {
|
override fun onRunJob(params: Params): Result {
|
||||||
GlobalScope.launch(Dispatchers.IO) {
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
val pendingUpdates = ExtensionGithubApi().checkforUpdates(context)
|
val pendingUpdates = ExtensionGithubApi().checkForUpdates(context)
|
||||||
if (pendingUpdates.isNotEmpty()) {
|
if (pendingUpdates.isNotEmpty()) {
|
||||||
val names = pendingUpdates.map { it.name }
|
val names = pendingUpdates.map { it.name }
|
||||||
val preferences: PreferencesHelper by injectLazy()
|
val preferences: PreferencesHelper by injectLazy()
|
||||||
|
@ -7,18 +7,16 @@ import com.github.salomonbrys.kotson.int
|
|||||||
import com.github.salomonbrys.kotson.string
|
import com.github.salomonbrys.kotson.string
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
import com.google.gson.JsonArray
|
import com.google.gson.JsonArray
|
||||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import eu.kanade.tachiyomi.extension.model.LoadResult
|
import eu.kanade.tachiyomi.extension.model.LoadResult
|
||||||
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
|
import eu.kanade.tachiyomi.network.await
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import eu.kanade.tachiyomi.network.await
|
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.lang.Exception
|
|
||||||
|
|
||||||
internal class ExtensionGithubApi {
|
internal class ExtensionGithubApi {
|
||||||
|
|
||||||
@ -34,7 +32,7 @@ internal class ExtensionGithubApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun checkforUpdates(context: Context): List<Extension.Installed> {
|
suspend fun checkForUpdates(context: Context): List<Extension.Installed> {
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
val call = GET("$REPO_URL/index.json")
|
val call = GET("$REPO_URL/index.json")
|
||||||
val response = network.client.newCall(call).await()
|
val response = network.client.newCall(call).await()
|
||||||
|
@ -7,21 +7,23 @@ import android.os.Handler
|
|||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
import android.webkit.WebSettings
|
import android.webkit.WebSettings
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
|
import android.widget.Toast
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.util.system.WebViewClientCompat
|
import eu.kanade.tachiyomi.util.system.WebViewClientCompat
|
||||||
import okhttp3.Cookie
|
import eu.kanade.tachiyomi.util.system.isOutdated
|
||||||
import okhttp3.Interceptor
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import okhttp3.Request
|
|
||||||
import okhttp3.Response
|
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.concurrent.CountDownLatch
|
import java.util.concurrent.CountDownLatch
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
import okhttp3.Cookie
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class CloudflareInterceptor(private val context: Context) : Interceptor {
|
class CloudflareInterceptor(private val context: Context) : Interceptor {
|
||||||
|
|
||||||
private val serverCheck = arrayOf("cloudflare-nginx", "cloudflare")
|
|
||||||
|
|
||||||
private val handler = Handler(Looper.getMainLooper())
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
|
|
||||||
private val networkHelper: NetworkHelper by injectLazy()
|
private val networkHelper: NetworkHelper by injectLazy()
|
||||||
@ -43,16 +45,28 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
|||||||
val response = chain.proceed(originalRequest)
|
val response = chain.proceed(originalRequest)
|
||||||
|
|
||||||
// Check if Cloudflare anti-bot is on
|
// Check if Cloudflare anti-bot is on
|
||||||
if (response.code == 503 && response.header("Server") in serverCheck) {
|
if (response.code != 503 || response.header("Server") !in SERVER_CHECK) {
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
response.close()
|
response.close()
|
||||||
networkHelper.cookieManager.remove(originalRequest.url, listOf("__cfduid", "cf_clearance"), 0)
|
networkHelper.cookieManager.remove(originalRequest.url, COOKIE_NAMES, 0)
|
||||||
val oldCookie = networkHelper.cookieManager.get(originalRequest.url)
|
val oldCookie = networkHelper.cookieManager.get(originalRequest.url)
|
||||||
.firstOrNull { it.name == "cf_clearance" }
|
.firstOrNull { it.name == "cf_clearance" }
|
||||||
return if (resolveWithWebView(originalRequest, oldCookie)) {
|
resolveWithWebView(originalRequest, oldCookie)
|
||||||
chain.proceed(originalRequest)
|
|
||||||
|
// Avoid use empty User-Agent
|
||||||
|
return if (originalRequest.header("User-Agent").isNullOrEmpty()) {
|
||||||
|
val newRequest = originalRequest
|
||||||
|
.newBuilder()
|
||||||
|
.removeHeader("User-Agent")
|
||||||
|
.addHeader("User-Agent",
|
||||||
|
DEFAULT_USERAGENT)
|
||||||
|
.build()
|
||||||
|
chain.proceed(newRequest)
|
||||||
} else {
|
} else {
|
||||||
throw IOException("Failed to bypass Cloudflare!")
|
chain.proceed(originalRequest)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
// Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that
|
// Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that
|
||||||
@ -61,41 +75,45 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("SetJavaScriptEnabled")
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
private fun resolveWithWebView(request: Request, oldCookie: Cookie?): Boolean {
|
private fun resolveWithWebView(request: Request, oldCookie: Cookie?) {
|
||||||
// We need to lock this thread until the WebView finds the challenge solution url, because
|
// We need to lock this thread until the WebView finds the challenge solution url, because
|
||||||
// OkHttp doesn't support asynchronous interceptors.
|
// OkHttp doesn't support asynchronous interceptors.
|
||||||
val latch = CountDownLatch(1)
|
val latch = CountDownLatch(1)
|
||||||
|
|
||||||
var webView: WebView? = null
|
var webView: WebView? = null
|
||||||
|
|
||||||
var challengeFound = false
|
var challengeFound = false
|
||||||
var cloudflareBypassed = false
|
var cloudflareBypassed = false
|
||||||
|
var isWebviewOutdated = false
|
||||||
|
|
||||||
val origRequestUrl = request.url.toString()
|
val origRequestUrl = request.url.toString()
|
||||||
val headers = request.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }
|
val headers = request.headers.toMultimap().mapValues { it.value.getOrNull(0) ?: "" }
|
||||||
|
val withUserAgent = request.header("User-Agent").isNullOrEmpty()
|
||||||
|
|
||||||
handler.post {
|
handler.post {
|
||||||
val view = WebView(context.applicationContext)
|
val webview = WebView(context)
|
||||||
webView = view
|
webView = webview
|
||||||
view.settings.javaScriptEnabled = true
|
webview.settings.javaScriptEnabled = true
|
||||||
view.settings.userAgentString = request.header("User-Agent")
|
|
||||||
view.webViewClient = object : WebViewClientCompat() {
|
|
||||||
|
|
||||||
|
// Avoid set empty User-Agent, Chromium WebView will reset to default if empty
|
||||||
|
webview.settings.userAgentString = request.header("User-Agent")
|
||||||
|
?: DEFAULT_USERAGENT
|
||||||
|
|
||||||
|
webview.webViewClient = object : WebViewClientCompat() {
|
||||||
override fun onPageFinished(view: WebView, url: String) {
|
override fun onPageFinished(view: WebView, url: String) {
|
||||||
fun isCloudFlareBypassed(): Boolean {
|
fun isCloudFlareBypassed(): Boolean {
|
||||||
return networkHelper.cookieManager.get(origRequestUrl.toHttpUrl())
|
return networkHelper.cookieManager.get(origRequestUrl.toHttpUrl())
|
||||||
.firstOrNull { it.name == "cf_clearance" }
|
.firstOrNull { it.name == "cf_clearance" }
|
||||||
.let { it != null && it != oldCookie }
|
.let { it != null && (it != oldCookie || withUserAgent) }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isCloudFlareBypassed()) {
|
if (isCloudFlareBypassed()) {
|
||||||
cloudflareBypassed = true
|
cloudflareBypassed = true
|
||||||
latch.countDown()
|
latch.countDown()
|
||||||
}
|
}
|
||||||
// Http error codes are only received since M
|
|
||||||
|
// HTTP error codes are only received since M
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
|
||||||
url == origRequestUrl && !challengeFound
|
url == origRequestUrl && !challengeFound
|
||||||
) {
|
) {
|
||||||
@ -122,6 +140,7 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
webView?.loadUrl(origRequestUrl, headers)
|
webView?.loadUrl(origRequestUrl, headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,10 +149,28 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
|
|||||||
latch.await(12, TimeUnit.SECONDS)
|
latch.await(12, TimeUnit.SECONDS)
|
||||||
|
|
||||||
handler.post {
|
handler.post {
|
||||||
|
if (!cloudflareBypassed) {
|
||||||
|
isWebviewOutdated = webView?.isOutdated() == true
|
||||||
|
}
|
||||||
|
|
||||||
webView?.stopLoading()
|
webView?.stopLoading()
|
||||||
webView?.destroy()
|
webView?.destroy()
|
||||||
}
|
}
|
||||||
return cloudflareBypassed
|
|
||||||
|
// Throw exception if we failed to bypass Cloudflare
|
||||||
|
if (!cloudflareBypassed) {
|
||||||
|
// Prompt user to update WebView if it seems too outdated
|
||||||
|
if (isWebviewOutdated) {
|
||||||
|
context.toast(R.string.information_webview_outdated, Toast.LENGTH_LONG)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw Exception(context.getString(R.string.information_cloudflare_bypass_failure))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val SERVER_CHECK = arrayOf("cloudflare-nginx", "cloudflare")
|
||||||
|
private val COOKIE_NAMES = listOf("__cfduid", "cf_clearance")
|
||||||
|
private const val DEFAULT_USERAGENT = "Mozilla/5.0 (Windows NT 6.3; WOW64)"
|
||||||
|
}
|
||||||
}
|
}
|
@ -5,8 +5,11 @@ import okhttp3.*
|
|||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Producer
|
import rx.Producer
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
|
import java.io.BufferedReader
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
|
import java.io.InputStreamReader
|
||||||
import java.util.concurrent.atomic.AtomicBoolean
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
|
import java.util.zip.GZIPInputStream
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
import kotlin.coroutines.resumeWithException
|
import kotlin.coroutines.resumeWithException
|
||||||
|
|
||||||
@ -94,3 +97,23 @@ fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListene
|
|||||||
|
|
||||||
return progressClient.newCall(request)
|
return progressClient.newCall(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Response.consumeBody(): String? {
|
||||||
|
use {
|
||||||
|
if (it.code != 200) throw Exception("HTTP error ${it.code}")
|
||||||
|
return it.body?.string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Response.consumeXmlBody(): String? {
|
||||||
|
use { res ->
|
||||||
|
if (res.code != 200) throw Exception("Export list error")
|
||||||
|
BufferedReader(InputStreamReader(GZIPInputStream(res.body?.source()?.inputStream()))).use { reader ->
|
||||||
|
val sb = StringBuilder()
|
||||||
|
reader.forEachLine { line ->
|
||||||
|
sb.append(line)
|
||||||
|
}
|
||||||
|
return sb.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -20,6 +20,10 @@ class CenteredToolbar@JvmOverloads constructor(context: Context, attrs: Attribut
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
toolbar_title.text = context.getString(resId)
|
toolbar_title.text = context.getString(resId)
|
||||||
|
post {
|
||||||
|
toolbar_title.text = context.getString(resId)
|
||||||
|
requestLayout()
|
||||||
|
}
|
||||||
super.setTitle(null)
|
super.setTitle(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -31,6 +35,10 @@ class CenteredToolbar@JvmOverloads constructor(context: Context, attrs: Attribut
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
toolbar_title.text = title
|
toolbar_title.text = title
|
||||||
|
post {
|
||||||
|
toolbar_title.text = title
|
||||||
|
requestLayout()
|
||||||
|
}
|
||||||
super.setTitle(null)
|
super.setTitle(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ import com.bluelinelabs.conductor.ControllerChangeHandler
|
|||||||
import com.bluelinelabs.conductor.ControllerChangeType
|
import com.bluelinelabs.conductor.ControllerChangeType
|
||||||
import com.bluelinelabs.conductor.RouterTransaction
|
import com.bluelinelabs.conductor.RouterTransaction
|
||||||
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
|
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
|
||||||
import com.jakewharton.rxbinding.support.v7.widget.queryTextChangeEvents
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
import eu.davidea.flexibleadapter.items.IFlexible
|
import eu.davidea.flexibleadapter.items.IFlexible
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
@ -26,17 +26,20 @@ import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
|||||||
import eu.kanade.tachiyomi.ui.catalogue.browse.BrowseCatalogueController
|
import eu.kanade.tachiyomi.ui.catalogue.browse.BrowseCatalogueController
|
||||||
import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchController
|
import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchController
|
||||||
import eu.kanade.tachiyomi.ui.catalogue.latest.LatestUpdatesController
|
import eu.kanade.tachiyomi.ui.catalogue.latest.LatestUpdatesController
|
||||||
|
import eu.kanade.tachiyomi.ui.extension.SettingsExtensionsController
|
||||||
import eu.kanade.tachiyomi.ui.main.RootSearchInterface
|
import eu.kanade.tachiyomi.ui.main.RootSearchInterface
|
||||||
import eu.kanade.tachiyomi.ui.setting.SettingsSourcesController
|
import eu.kanade.tachiyomi.ui.setting.SettingsSourcesController
|
||||||
import eu.kanade.tachiyomi.util.view.RecyclerWindowInsetsListener
|
|
||||||
import eu.kanade.tachiyomi.util.view.applyWindowInsetsForRootController
|
import eu.kanade.tachiyomi.util.view.applyWindowInsetsForRootController
|
||||||
import eu.kanade.tachiyomi.util.view.scrollViewWith
|
import eu.kanade.tachiyomi.util.view.scrollViewWith
|
||||||
|
import eu.kanade.tachiyomi.util.view.setOnQueryTextChangeListener
|
||||||
import eu.kanade.tachiyomi.widget.preference.SourceLoginDialog
|
import eu.kanade.tachiyomi.widget.preference.SourceLoginDialog
|
||||||
import kotlinx.android.parcel.Parcelize
|
import kotlinx.android.parcel.Parcelize
|
||||||
import kotlinx.android.synthetic.main.catalogue_main_controller.*
|
import kotlinx.android.synthetic.main.catalogue_main_controller.*
|
||||||
|
import kotlinx.android.synthetic.main.extensions_bottom_sheet.*
|
||||||
import kotlinx.android.synthetic.main.main_activity.*
|
import kotlinx.android.synthetic.main.main_activity.*
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This controller shows and manages the different catalogues enabled by the user.
|
* This controller shows and manages the different catalogues enabled by the user.
|
||||||
@ -50,6 +53,7 @@ class CatalogueController : NucleusController<CataloguePresenter>(),
|
|||||||
FlexibleAdapter.OnItemClickListener,
|
FlexibleAdapter.OnItemClickListener,
|
||||||
CatalogueAdapter.OnBrowseClickListener,
|
CatalogueAdapter.OnBrowseClickListener,
|
||||||
RootSearchInterface,
|
RootSearchInterface,
|
||||||
|
|
||||||
CatalogueAdapter.OnLatestClickListener {
|
CatalogueAdapter.OnLatestClickListener {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -62,6 +66,13 @@ class CatalogueController : NucleusController<CataloguePresenter>(),
|
|||||||
*/
|
*/
|
||||||
private var adapter: CatalogueAdapter? = null
|
private var adapter: CatalogueAdapter? = null
|
||||||
|
|
||||||
|
var extQuery = ""
|
||||||
|
private set
|
||||||
|
|
||||||
|
var headerHeight = 0
|
||||||
|
|
||||||
|
var customTitle = ""
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when controller is initialized.
|
* Called when controller is initialized.
|
||||||
*/
|
*/
|
||||||
@ -76,7 +87,9 @@ class CatalogueController : NucleusController<CataloguePresenter>(),
|
|||||||
* @return title.
|
* @return title.
|
||||||
*/
|
*/
|
||||||
override fun getTitle(): String? {
|
override fun getTitle(): String? {
|
||||||
return applicationContext?.getString(R.string.label_catalogues)
|
return if (ext_bottom_sheet.sheetBehavior?.state == BottomSheetBehavior.STATE_EXPANDED)
|
||||||
|
applicationContext?.getString(R.string.label_extensions)
|
||||||
|
else applicationContext?.getString(R.string.label_catalogues)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -114,11 +127,49 @@ class CatalogueController : NucleusController<CataloguePresenter>(),
|
|||||||
recycler.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(view.context)
|
recycler.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(view.context)
|
||||||
recycler.adapter = adapter
|
recycler.adapter = adapter
|
||||||
recycler.addItemDecoration(SourceDividerItemDecoration(view.context))
|
recycler.addItemDecoration(SourceDividerItemDecoration(view.context))
|
||||||
recycler.setOnApplyWindowInsetsListener(RecyclerWindowInsetsListener)
|
val attrsArray = intArrayOf(android.R.attr.actionBarSize)
|
||||||
|
val array = view.context.obtainStyledAttributes(attrsArray)
|
||||||
scrollViewWith(recycler)
|
val appBarHeight = array.getDimensionPixelSize(0, 0)
|
||||||
|
array.recycle()
|
||||||
|
scrollViewWith(recycler) {
|
||||||
|
headerHeight = it.systemWindowInsetTop + appBarHeight
|
||||||
|
}
|
||||||
|
|
||||||
requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 301)
|
requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 301)
|
||||||
|
ext_bottom_sheet.onCreate(this)
|
||||||
|
|
||||||
|
ext_bottom_sheet.sheetBehavior?.addBottomSheetCallback(object : BottomSheetBehavior
|
||||||
|
.BottomSheetCallback() {
|
||||||
|
override fun onSlide(bottomSheet: View, progress: Float) {
|
||||||
|
shadow2.alpha = (1 - max(0f, progress)) * 0.25f
|
||||||
|
sheet_layout.alpha = 1 - progress
|
||||||
|
activity?.appbar?.y = max(activity!!.appbar.y, -headerHeight * (1 - progress))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStateChanged(p0: View, state: Int) {
|
||||||
|
if (state == BottomSheetBehavior.STATE_EXPANDED) activity?.appbar?.y = 0f
|
||||||
|
if (state == BottomSheetBehavior.STATE_EXPANDED ||
|
||||||
|
state == BottomSheetBehavior.STATE_COLLAPSED)
|
||||||
|
sheet_layout.alpha =
|
||||||
|
if (state == BottomSheetBehavior.STATE_COLLAPSED) 1f else 0f
|
||||||
|
|
||||||
|
retainViewMode = if (state == BottomSheetBehavior.STATE_EXPANDED)
|
||||||
|
RetainViewMode.RETAIN_DETACH else RetainViewMode.RELEASE_DETACH
|
||||||
|
activity?.invalidateOptionsMenu()
|
||||||
|
setTitle()
|
||||||
|
sheet_layout.isClickable = state == BottomSheetBehavior.STATE_COLLAPSED
|
||||||
|
sheet_layout.isFocusable = state == BottomSheetBehavior.STATE_COLLAPSED
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun handleRootBack(): Boolean {
|
||||||
|
if (ext_bottom_sheet.sheetBehavior?.state != BottomSheetBehavior.STATE_COLLAPSED) {
|
||||||
|
ext_bottom_sheet.sheetBehavior?.state = BottomSheetBehavior.STATE_COLLAPSED
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroyView(view: View) {
|
override fun onDestroyView(view: View) {
|
||||||
@ -129,6 +180,7 @@ class CatalogueController : NucleusController<CataloguePresenter>(),
|
|||||||
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
|
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
|
||||||
super.onChangeStarted(handler, type)
|
super.onChangeStarted(handler, type)
|
||||||
if (!type.isPush && handler is SettingsSourcesFadeChangeHandler) {
|
if (!type.isPush && handler is SettingsSourcesFadeChangeHandler) {
|
||||||
|
ext_bottom_sheet.updateExtTitle()
|
||||||
presenter.updateSources()
|
presenter.updateSources()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -192,6 +244,25 @@ class CatalogueController : NucleusController<CataloguePresenter>(),
|
|||||||
* @param inflater used to load the menu xml.
|
* @param inflater used to load the menu xml.
|
||||||
*/
|
*/
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
|
if (ext_bottom_sheet.sheetBehavior?.state == BottomSheetBehavior.STATE_EXPANDED) {
|
||||||
|
// Inflate menu
|
||||||
|
inflater.inflate(R.menu.extension_main, menu)
|
||||||
|
|
||||||
|
// Initialize search option.
|
||||||
|
val searchItem = menu.findItem(R.id.action_search)
|
||||||
|
val searchView = searchItem.actionView as SearchView
|
||||||
|
|
||||||
|
// Change hint to show global search.
|
||||||
|
searchView.queryHint = applicationContext?.getString(R.string.search_extensions)
|
||||||
|
|
||||||
|
// Create query listener which opens the global search view.
|
||||||
|
setOnQueryTextChangeListener(searchView) {
|
||||||
|
extQuery = it ?: ""
|
||||||
|
ext_bottom_sheet.drawExtensions()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
// Inflate menu
|
// Inflate menu
|
||||||
inflater.inflate(R.menu.catalogue_main, menu)
|
inflater.inflate(R.menu.catalogue_main, menu)
|
||||||
|
|
||||||
@ -203,9 +274,11 @@ class CatalogueController : NucleusController<CataloguePresenter>(),
|
|||||||
searchView.queryHint = applicationContext?.getString(R.string.action_global_search_hint)
|
searchView.queryHint = applicationContext?.getString(R.string.action_global_search_hint)
|
||||||
|
|
||||||
// Create query listener which opens the global search view.
|
// Create query listener which opens the global search view.
|
||||||
searchView.queryTextChangeEvents()
|
setOnQueryTextChangeListener(searchView, true) {
|
||||||
.filter { it.isSubmitted }
|
if (!it.isNullOrBlank()) performGlobalSearch(it)
|
||||||
.subscribeUntilDestroy { performGlobalSearch(it.queryText().toString()) }
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun performGlobalSearch(query: String){
|
private fun performGlobalSearch(query: String){
|
||||||
@ -222,9 +295,18 @@ class CatalogueController : NucleusController<CataloguePresenter>(),
|
|||||||
when (item.itemId) {
|
when (item.itemId) {
|
||||||
// Initialize option to open catalogue settings.
|
// Initialize option to open catalogue settings.
|
||||||
R.id.action_filter -> {
|
R.id.action_filter -> {
|
||||||
router.pushController((RouterTransaction.with(SettingsSourcesController()))
|
val controller =
|
||||||
.popChangeHandler(SettingsSourcesFadeChangeHandler())
|
if (ext_bottom_sheet.sheetBehavior?.state == BottomSheetBehavior.STATE_EXPANDED)
|
||||||
.pushChangeHandler(FadeChangeHandler()))
|
SettingsExtensionsController()
|
||||||
|
else SettingsSourcesController()
|
||||||
|
router.pushController(
|
||||||
|
(RouterTransaction.with(controller)).popChangeHandler(
|
||||||
|
SettingsSourcesFadeChangeHandler()
|
||||||
|
).pushChangeHandler(FadeChangeHandler())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
R.id.action_dismiss -> {
|
||||||
|
ext_bottom_sheet.sheetBehavior?.state = BottomSheetBehavior.STATE_COLLAPSED
|
||||||
}
|
}
|
||||||
else -> return super.onOptionsItemSelected(item)
|
else -> return super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ import rx.Subscription
|
|||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.util.*
|
import java.util.TreeMap
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -3,17 +3,19 @@ package eu.kanade.tachiyomi.ui.extension
|
|||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
import eu.davidea.flexibleadapter.items.IFlexible
|
import eu.davidea.flexibleadapter.items.IFlexible
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.ui.extension.ExtensionAdapter.OnButtonClickListener
|
||||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adapter that holds the catalogue cards.
|
* Adapter that holds the catalogue cards.
|
||||||
*
|
*
|
||||||
* @param controller instance of [ExtensionController].
|
* @param listener instance of [OnButtonClickListener].
|
||||||
*/
|
*/
|
||||||
class ExtensionAdapter(val controller: ExtensionController) :
|
class ExtensionAdapter(val listener: OnButtonClickListener) :
|
||||||
FlexibleAdapter<IFlexible<*>>(null, controller, true) {
|
FlexibleAdapter<IFlexible<*>>(null, listener, true) {
|
||||||
|
|
||||||
val cardBackground = controller.activity!!.getResourceColor(R.attr.background_card)
|
val cardBackground = (listener as ExtensionBottomSheet).context.getResourceColor(R.attr
|
||||||
|
.background_card)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
setDisplayHeadersAtStartUp(true)
|
setDisplayHeadersAtStartUp(true)
|
||||||
@ -22,7 +24,7 @@ class ExtensionAdapter(val controller: ExtensionController) :
|
|||||||
/**
|
/**
|
||||||
* Listener for browse item clicks.
|
* Listener for browse item clicks.
|
||||||
*/
|
*/
|
||||||
val buttonClickListener: ExtensionAdapter.OnButtonClickListener = controller
|
val buttonClickListener: ExtensionAdapter.OnButtonClickListener = listener
|
||||||
|
|
||||||
interface OnButtonClickListener {
|
interface OnButtonClickListener {
|
||||||
fun onButtonClick(position: Int)
|
fun onButtonClick(position: Int)
|
||||||
|
@ -0,0 +1,153 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.extension
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
|
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||||
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
|
import eu.kanade.tachiyomi.extension.model.InstallStep
|
||||||
|
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import rx.Observable
|
||||||
|
import rx.Subscription
|
||||||
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Presenter of [ExtensionController].
|
||||||
|
*/
|
||||||
|
open class ExtensionBottomPresenter(
|
||||||
|
private val bottomSheet: ExtensionBottomSheet,
|
||||||
|
private val extensionManager: ExtensionManager = Injekt.get(),
|
||||||
|
private val preferences: PreferencesHelper = Injekt.get()
|
||||||
|
) : CoroutineScope {
|
||||||
|
|
||||||
|
override var coroutineContext: CoroutineContext = Job() + Dispatchers.Default
|
||||||
|
|
||||||
|
private var extensions = emptyList<ExtensionItem>()
|
||||||
|
|
||||||
|
private var currentDownloads = hashMapOf<String, InstallStep>()
|
||||||
|
|
||||||
|
fun onCreate() {
|
||||||
|
extensionManager.findAvailableExtensions()
|
||||||
|
bindToExtensionsObservable()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun bindToExtensionsObservable(): Subscription {
|
||||||
|
val installedObservable = extensionManager.getInstalledExtensionsObservable()
|
||||||
|
val untrustedObservable = extensionManager.getUntrustedExtensionsObservable()
|
||||||
|
val availableObservable = extensionManager.getAvailableExtensionsObservable()
|
||||||
|
.startWith(emptyList<Extension.Available>())
|
||||||
|
|
||||||
|
return Observable.combineLatest(installedObservable, untrustedObservable, availableObservable)
|
||||||
|
{ installed, untrusted, available -> Triple(installed, untrusted, available) }
|
||||||
|
.debounce(100, TimeUnit.MILLISECONDS)
|
||||||
|
.map(::toItems)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe {
|
||||||
|
bottomSheet.setExtensions(extensions)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun toItems(tuple: ExtensionTuple): List<ExtensionItem> {
|
||||||
|
val context = Injekt.get<Application>()
|
||||||
|
val activeLangs = preferences.enabledLanguages().getOrDefault()
|
||||||
|
|
||||||
|
val (installed, untrusted, available) = tuple
|
||||||
|
|
||||||
|
val items = mutableListOf<ExtensionItem>()
|
||||||
|
|
||||||
|
val installedSorted = installed.sortedWith(compareBy({ !it.hasUpdate }, { !it.isObsolete }, { it.pkgName }))
|
||||||
|
val untrustedSorted = untrusted.sortedBy { it.pkgName }
|
||||||
|
val availableSorted = available
|
||||||
|
// Filter out already installed extensions and disabled languages
|
||||||
|
.filter { avail -> installed.none { it.pkgName == avail.pkgName }
|
||||||
|
&& untrusted.none { it.pkgName == avail.pkgName }
|
||||||
|
&& (avail.lang in activeLangs || avail.lang == "all")}
|
||||||
|
.sortedBy { it.pkgName }
|
||||||
|
|
||||||
|
if (installedSorted.isNotEmpty() || untrustedSorted.isNotEmpty()) {
|
||||||
|
val header = ExtensionGroupItem(context.getString(R.string.ext_installed), installedSorted.size + untrustedSorted.size)
|
||||||
|
items += installedSorted.map { extension ->
|
||||||
|
ExtensionItem(extension, header, currentDownloads[extension.pkgName])
|
||||||
|
}
|
||||||
|
items += untrustedSorted.map { extension ->
|
||||||
|
ExtensionItem(extension, header)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (availableSorted.isNotEmpty()) {
|
||||||
|
val availableGroupedByLang = availableSorted
|
||||||
|
.groupBy { LocaleHelper.getDisplayName(it.lang, context) }
|
||||||
|
.toSortedMap()
|
||||||
|
|
||||||
|
availableGroupedByLang
|
||||||
|
.forEach {
|
||||||
|
val header = ExtensionGroupItem(it.key, it.value.size)
|
||||||
|
items += it.value.map { extension ->
|
||||||
|
ExtensionItem(extension, header, currentDownloads[extension.pkgName])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.extensions = items
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getExtensionUpdateCount():Int = preferences.extensionUpdatesCount().getOrDefault()
|
||||||
|
fun getAutoCheckPref() = preferences.automaticExtUpdates()
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun updateInstallStep(extension: Extension, state: InstallStep): ExtensionItem? {
|
||||||
|
val extensions = extensions.toMutableList()
|
||||||
|
val position = extensions.indexOfFirst { it.extension.pkgName == extension.pkgName }
|
||||||
|
|
||||||
|
return if (position != -1) {
|
||||||
|
val item = extensions[position].copy(installStep = state)
|
||||||
|
extensions[position] = item
|
||||||
|
|
||||||
|
this.extensions = extensions
|
||||||
|
item
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun installExtension(extension: Extension.Available) {
|
||||||
|
extensionManager.installExtension(extension).subscribeToInstallUpdate(extension)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateExtension(extension: Extension.Installed) {
|
||||||
|
extensionManager.updateExtension(extension).subscribeToInstallUpdate(extension)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Observable<InstallStep>.subscribeToInstallUpdate(extension: Extension) {
|
||||||
|
this.doOnNext { currentDownloads[extension.pkgName] = it }
|
||||||
|
.doOnUnsubscribe { currentDownloads.remove(extension.pkgName) }
|
||||||
|
.map { state -> updateInstallStep(extension, state) }
|
||||||
|
.subscribe { item ->
|
||||||
|
if (item != null) {
|
||||||
|
bottomSheet.downloadUpdate(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun uninstallExtension(pkgName: String) {
|
||||||
|
extensionManager.uninstallExtension(pkgName)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun findAvailableExtensions() {
|
||||||
|
extensionManager.findAvailableExtensions()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun trustSignature(signatureHash: String) {
|
||||||
|
extensionManager.trustSignature(signatureHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,225 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.extension
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.CheckBox
|
||||||
|
import android.widget.CompoundButton
|
||||||
|
import android.widget.LinearLayout
|
||||||
|
import com.f2prateek.rx.preferences.Preference
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
|
import eu.davidea.flexibleadapter.items.AbstractHeaderItem
|
||||||
|
import eu.davidea.flexibleadapter.items.IFlexible
|
||||||
|
import eu.davidea.viewholders.FlexibleViewHolder
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||||
|
import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
|
||||||
|
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||||
|
import eu.kanade.tachiyomi.util.view.RecyclerWindowInsetsListener
|
||||||
|
import eu.kanade.tachiyomi.util.view.doOnApplyWindowInsets
|
||||||
|
import eu.kanade.tachiyomi.util.view.updateLayoutParams
|
||||||
|
import kotlinx.android.synthetic.main.extensions_bottom_sheet.view.*
|
||||||
|
|
||||||
|
class ExtensionBottomSheet @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null)
|
||||||
|
: LinearLayout(context, attrs),
|
||||||
|
ExtensionAdapter.OnButtonClickListener,
|
||||||
|
FlexibleAdapter.OnItemClickListener,
|
||||||
|
FlexibleAdapter.OnItemLongClickListener,
|
||||||
|
ExtensionTrustDialog.Listener {
|
||||||
|
|
||||||
|
var sheetBehavior: BottomSheetBehavior<*>? = null
|
||||||
|
lateinit var autoCheckItem:AutoCheckItem
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapter containing the list of manga from the catalogue.
|
||||||
|
*/
|
||||||
|
private var adapter: FlexibleAdapter<IFlexible<*>>? = null
|
||||||
|
|
||||||
|
val presenter = ExtensionBottomPresenter(this)
|
||||||
|
|
||||||
|
private var extensions: List<ExtensionItem> = emptyList()
|
||||||
|
|
||||||
|
lateinit var controller: CatalogueController
|
||||||
|
|
||||||
|
fun onCreate(controller: CatalogueController) {
|
||||||
|
// Initialize adapter, scroll listener and recycler views
|
||||||
|
autoCheckItem = AutoCheckItem(presenter.getAutoCheckPref())
|
||||||
|
adapter = ExtensionAdapter(this)
|
||||||
|
sheetBehavior = BottomSheetBehavior.from(this)
|
||||||
|
// Create recycler and set adapter.
|
||||||
|
ext_recycler.layoutManager = androidx.recyclerview.widget.LinearLayoutManager(context)
|
||||||
|
ext_recycler.adapter = adapter
|
||||||
|
ext_recycler.addItemDecoration(ExtensionDividerItemDecoration(context))
|
||||||
|
ext_recycler.setOnApplyWindowInsetsListener(RecyclerWindowInsetsListener)
|
||||||
|
this.controller = controller
|
||||||
|
presenter.onCreate()
|
||||||
|
updateExtTitle()
|
||||||
|
|
||||||
|
val attrsArray = intArrayOf(android.R.attr.actionBarSize)
|
||||||
|
val array = context.obtainStyledAttributes(attrsArray)
|
||||||
|
val headerHeight = array.getDimensionPixelSize(0, 0)
|
||||||
|
array.recycle()
|
||||||
|
ext_recycler.doOnApplyWindowInsets { _, windowInsets, _ ->
|
||||||
|
ext_recycler.updateLayoutParams<LayoutParams> {
|
||||||
|
topMargin = windowInsets.systemWindowInsetTop + headerHeight -
|
||||||
|
(sheet_layout.height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sheet_layout.setOnClickListener {
|
||||||
|
if (sheetBehavior?.state != BottomSheetBehavior.STATE_EXPANDED) {
|
||||||
|
sheetBehavior?.state = BottomSheetBehavior.STATE_EXPANDED
|
||||||
|
} else {
|
||||||
|
sheetBehavior?.state = BottomSheetBehavior.STATE_COLLAPSED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
presenter.getExtensionUpdateCount()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateExtTitle() {
|
||||||
|
val extCount = presenter.getExtensionUpdateCount()
|
||||||
|
title_text.text = if (extCount == 0) context.getString(R.string.label_extensions)
|
||||||
|
else resources.getQuantityString(R.plurals.extensions_updates_available, extCount,
|
||||||
|
extCount)
|
||||||
|
|
||||||
|
title_text.setTextColor(context.getResourceColor(
|
||||||
|
if (extCount == 0) R.attr.actionBarTintColor else R.attr.colorAccent))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onButtonClick(position: Int) {
|
||||||
|
val extension = (adapter?.getItem(position) as? ExtensionItem)?.extension ?: return
|
||||||
|
when (extension) {
|
||||||
|
is Extension.Installed -> {
|
||||||
|
if (!extension.hasUpdate) {
|
||||||
|
openDetails(extension)
|
||||||
|
} else {
|
||||||
|
presenter.updateExtension(extension)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is Extension.Available -> {
|
||||||
|
presenter.installExtension(extension)
|
||||||
|
}
|
||||||
|
is Extension.Untrusted -> {
|
||||||
|
openTrustDialog(extension)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemClick(view: View?, position: Int): Boolean {
|
||||||
|
val extension = (adapter?.getItem(position) as? ExtensionItem)?.extension ?: return false
|
||||||
|
if (extension is Extension.Installed) {
|
||||||
|
openDetails(extension)
|
||||||
|
} else if (extension is Extension.Untrusted) {
|
||||||
|
openTrustDialog(extension)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onItemLongClick(position: Int) {
|
||||||
|
val extension = (adapter?.getItem(position) as? ExtensionItem)?.extension ?: return
|
||||||
|
if (extension is Extension.Installed || extension is Extension.Untrusted) {
|
||||||
|
uninstallExtension(extension.pkgName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openDetails(extension: Extension.Installed) {
|
||||||
|
val controller = ExtensionDetailsController(extension.pkgName)
|
||||||
|
this.controller.router.pushController(controller.withFadeTransaction())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openTrustDialog(extension: Extension.Untrusted) {
|
||||||
|
ExtensionTrustDialog(this, extension.signatureHash, extension.pkgName)
|
||||||
|
.showDialog(controller.router)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setExtensions(extensions: List<ExtensionItem>) {
|
||||||
|
//ext_swipe_refresh?.isRefreshing = false
|
||||||
|
this.extensions = extensions
|
||||||
|
controller.presenter.updateSources()
|
||||||
|
drawExtensions()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun drawExtensions() {
|
||||||
|
if (!controller.extQuery.isBlank()) {
|
||||||
|
adapter?.updateDataSet(
|
||||||
|
extensions.filter {
|
||||||
|
it.extension.name.contains(controller.extQuery, ignoreCase = true)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
adapter?.updateDataSet(extensions)
|
||||||
|
}
|
||||||
|
updateExtTitle()
|
||||||
|
setLastUsedSource()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called to set the last used catalogue at the top of the view.
|
||||||
|
*/
|
||||||
|
private fun setLastUsedSource() {
|
||||||
|
adapter?.removeAllScrollableHeaders()
|
||||||
|
adapter?.addScrollableHeader(autoCheckItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun downloadUpdate(item: ExtensionItem) {
|
||||||
|
adapter?.updateItem(item, item.installStep)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun trustSignature(signatureHash: String) {
|
||||||
|
presenter.trustSignature(signatureHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun uninstallExtension(pkgName: String) {
|
||||||
|
presenter.uninstallExtension(pkgName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class AutoCheckItem(private val autoCheck: Preference<Boolean>) : AbstractHeaderItem<AutoCheckItem.AutoCheckHolder>() {
|
||||||
|
|
||||||
|
override fun getLayoutRes(): Int {
|
||||||
|
return R.layout.auto_ext_checkbox
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun createViewHolder(
|
||||||
|
view: View, adapter: FlexibleAdapter<IFlexible<*>>
|
||||||
|
): AutoCheckHolder {
|
||||||
|
return AutoCheckHolder(view, adapter, autoCheck)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun bindViewHolder(
|
||||||
|
adapter: FlexibleAdapter<IFlexible<*>>,
|
||||||
|
holder: AutoCheckHolder,
|
||||||
|
position: Int,
|
||||||
|
payloads: MutableList<Any?>?
|
||||||
|
) {
|
||||||
|
//holder.bind(autoCheck.getOrDefault())
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
return (this === other)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
class AutoCheckHolder(val view: View, private val adapter: FlexibleAdapter<IFlexible<*>>,
|
||||||
|
autoCheck: Preference<Boolean>) :
|
||||||
|
FlexibleViewHolder(view, adapter, true) {
|
||||||
|
private val autoCheckbox: CheckBox = view.findViewById(R.id.auto_checkbox)
|
||||||
|
|
||||||
|
init {
|
||||||
|
autoCheckbox.bindToPreference(autoCheck)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Binds a checkbox or switch view with a boolean preference.
|
||||||
|
*/
|
||||||
|
private fun CompoundButton.bindToPreference(pref: Preference<Boolean>) {
|
||||||
|
isChecked = pref.getOrDefault()
|
||||||
|
setOnCheckedChangeListener { _, isChecked -> pref.set(isChecked) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -17,7 +17,7 @@ import uy.kohesive.injekt.Injekt
|
|||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
private typealias ExtensionTuple
|
typealias ExtensionTuple
|
||||||
= Triple<List<Extension.Installed>, List<Extension.Untrusted>, List<Extension.Available>>
|
= Triple<List<Extension.Installed>, List<Extension.Untrusted>, List<Extension.Available>>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -3,18 +3,18 @@ package eu.kanade.tachiyomi.ui.extension
|
|||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
import com.bluelinelabs.conductor.Controller
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||||
|
|
||||||
class ExtensionTrustDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
|
class ExtensionTrustDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
|
||||||
where T : Controller, T: ExtensionTrustDialog.Listener {
|
where T: ExtensionTrustDialog.Listener {
|
||||||
|
|
||||||
|
lateinit var listener: Listener
|
||||||
constructor(target: T, signatureHash: String, pkgName: String) : this(Bundle().apply {
|
constructor(target: T, signatureHash: String, pkgName: String) : this(Bundle().apply {
|
||||||
putString(SIGNATURE_KEY, signatureHash)
|
putString(SIGNATURE_KEY, signatureHash)
|
||||||
putString(PKGNAME_KEY, pkgName)
|
putString(PKGNAME_KEY, pkgName)
|
||||||
}) {
|
}) {
|
||||||
targetController = target
|
listener = target
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||||
@ -22,10 +22,10 @@ class ExtensionTrustDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
|
|||||||
.title(R.string.untrusted_extension)
|
.title(R.string.untrusted_extension)
|
||||||
.message(R.string.untrusted_extension_message)
|
.message(R.string.untrusted_extension_message)
|
||||||
.positiveButton(R.string.ext_trust) {
|
.positiveButton(R.string.ext_trust) {
|
||||||
(targetController as? Listener)?.trustSignature(args.getString(SIGNATURE_KEY)!!)
|
listener.trustSignature(args.getString(SIGNATURE_KEY)!!)
|
||||||
}
|
}
|
||||||
.negativeButton(R.string.ext_uninstall) {
|
.negativeButton(R.string.ext_uninstall) {
|
||||||
(targetController as? Listener)?.uninstallExtension(args.getString(PKGNAME_KEY)!!)
|
listener.uninstallExtension(args.getString(PKGNAME_KEY)!!)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -183,5 +183,6 @@ class LibraryCategoryAdapter(val libraryListener: LibraryListener) :
|
|||||||
fun selectAll(position: Int)
|
fun selectAll(position: Int)
|
||||||
fun allSelected(position: Int): Boolean
|
fun allSelected(position: Int): Boolean
|
||||||
fun showCategories(position: Int, view: View)
|
fun showCategories(position: Int, view: View)
|
||||||
|
fun recyclerIsScrolling(): Boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -399,4 +399,5 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
|
|||||||
override fun selectAll(position: Int) { }
|
override fun selectAll(position: Int) { }
|
||||||
override fun allSelected(position: Int): Boolean = false
|
override fun allSelected(position: Int): Boolean = false
|
||||||
override fun showCategories(position: Int, view: View) { }
|
override fun showCategories(position: Int, view: View) { }
|
||||||
|
override fun recyclerIsScrolling() = false
|
||||||
}
|
}
|
||||||
|
@ -40,8 +40,7 @@ import eu.kanade.tachiyomi.data.preference.getOrDefault
|
|||||||
import eu.kanade.tachiyomi.ui.base.controller.BaseController
|
import eu.kanade.tachiyomi.ui.base.controller.BaseController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.TabbedController
|
import eu.kanade.tachiyomi.ui.base.controller.TabbedController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||||
import eu.kanade.tachiyomi.ui.download.DownloadController
|
import eu.kanade.tachiyomi.ui.library.filter.FilterBottomSheet
|
||||||
import eu.kanade.tachiyomi.ui.library.filter.SortFilterBottomSheet
|
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
import eu.kanade.tachiyomi.ui.main.RootSearchInterface
|
import eu.kanade.tachiyomi.ui.main.RootSearchInterface
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaDetailsController
|
import eu.kanade.tachiyomi.ui.manga.MangaDetailsController
|
||||||
@ -51,7 +50,7 @@ import eu.kanade.tachiyomi.ui.migration.manga.process.MigrationListController
|
|||||||
import eu.kanade.tachiyomi.ui.migration.manga.process.MigrationProcedureConfig
|
import eu.kanade.tachiyomi.ui.migration.manga.process.MigrationProcedureConfig
|
||||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||||
import eu.kanade.tachiyomi.util.system.launchUI
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import eu.kanade.tachiyomi.util.view.applyWindowInsetsForRootController
|
import eu.kanade.tachiyomi.util.view.applyWindowInsetsForRootController
|
||||||
import eu.kanade.tachiyomi.util.view.setOnQueryTextChangeListener
|
import eu.kanade.tachiyomi.util.view.setOnQueryTextChangeListener
|
||||||
import eu.kanade.tachiyomi.util.view.snack
|
import eu.kanade.tachiyomi.util.view.snack
|
||||||
@ -192,26 +191,22 @@ open class LibraryController(
|
|||||||
|
|
||||||
bottom_sheet.onGroupClicked = {
|
bottom_sheet.onGroupClicked = {
|
||||||
when (it) {
|
when (it) {
|
||||||
SortFilterBottomSheet.ACTION_REFRESH -> onRefresh()
|
FilterBottomSheet.ACTION_REFRESH -> onRefresh()
|
||||||
SortFilterBottomSheet.ACTION_FILTER -> onFilterChanged()
|
FilterBottomSheet.ACTION_FILTER -> onFilterChanged()
|
||||||
SortFilterBottomSheet.ACTION_SORT -> onSortChanged()
|
FilterBottomSheet.ACTION_HIDE_FILTER_TIP -> activity?.toast(R.string.hide_filters_tip)
|
||||||
SortFilterBottomSheet.ACTION_DISPLAY -> reattachAdapter()
|
|
||||||
SortFilterBottomSheet.ACTION_DOWNLOAD_BADGE -> presenter.requestDownloadBadgesUpdate()
|
|
||||||
SortFilterBottomSheet.ACTION_UNREAD_BADGE -> presenter.requestUnreadBadgesUpdate()
|
|
||||||
SortFilterBottomSheet.ACTION_CAT_SORT -> onCatSortChanged()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fab.setOnClickListener {
|
/* fab.setOnClickListener {
|
||||||
router.pushController(DownloadController().withFadeTransaction())
|
router.pushController(DownloadController().withFadeTransaction())
|
||||||
}
|
}*/
|
||||||
|
|
||||||
if (presenter.isDownloading()) {
|
/* if (presenter.isDownloading()) {
|
||||||
fab.scaleY = 1f
|
fab.scaleY = 1f
|
||||||
fab.scaleX = 1f
|
fab.scaleX = 1f
|
||||||
fab.isClickable = true
|
fab.isClickable = true
|
||||||
fab.isFocusable = true
|
fab.isFocusable = true
|
||||||
}
|
}*/
|
||||||
|
|
||||||
val config = resources?.configuration
|
val config = resources?.configuration
|
||||||
phoneLandscape = (config?.orientation == Configuration.ORIENTATION_LANDSCAPE &&
|
phoneLandscape = (config?.orientation == Configuration.ORIENTATION_LANDSCAPE &&
|
||||||
@ -290,14 +285,14 @@ open class LibraryController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun downloadStatusChanged(downloading: Boolean) {
|
override fun downloadStatusChanged(downloading: Boolean) {
|
||||||
launchUI {
|
/* launchUI {
|
||||||
val scale = if (downloading) 1f else 0f
|
val scale = if (downloading) 1f else 0f
|
||||||
val fab = fab ?: return@launchUI
|
val fab = fab ?: return@launchUI
|
||||||
fab.animate().scaleX(scale).scaleY(scale).setDuration(200).start()
|
fab.animate().scaleX(scale).scaleY(scale).setDuration(200).start()
|
||||||
fab.isClickable = downloading
|
fab.isClickable = downloading
|
||||||
fab.isFocusable = downloading
|
fab.isFocusable = downloading
|
||||||
bottom_sheet?.adjustFiltersMargin(downloading)
|
bottom_sheet?.adjustFiltersMargin(downloading)
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onUpdateManga(manga: LibraryManga) {
|
override fun onUpdateManga(manga: LibraryManga) {
|
||||||
|
@ -144,6 +144,7 @@ class LibraryHeaderItem(private val categoryF: (Int) -> Category, val catId: Int
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
private fun showCatSortOptions() {
|
private fun showCatSortOptions() {
|
||||||
|
if (adapter.libraryListener.recyclerIsScrolling()) return
|
||||||
val category =
|
val category =
|
||||||
(adapter.getItem(adapterPosition) as? LibraryHeaderItem)?.category ?: return
|
(adapter.getItem(adapterPosition) as? LibraryHeaderItem)?.category ?: return
|
||||||
// Create a PopupMenu, giving it the clicked view for an anchor
|
// Create a PopupMenu, giving it the clicked view for an anchor
|
||||||
|
@ -50,4 +50,9 @@ abstract class LibraryHolder(
|
|||||||
super.onItemReleased(position)
|
super.onItemReleased(position)
|
||||||
(adapter as? LibraryCategoryAdapter)?.libraryListener?.onItemReleased(position)
|
(adapter as? LibraryCategoryAdapter)?.libraryListener?.onItemReleased(position)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onLongClick(view: View?): Boolean {
|
||||||
|
super.onLongClick(view)
|
||||||
|
return false // !adapter.libraryListener.recyclerIsScrolling()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,21 @@
|
|||||||
package eu.kanade.tachiyomi.ui.library
|
package eu.kanade.tachiyomi.ui.library
|
||||||
|
|
||||||
|
import android.animation.Animator
|
||||||
|
import android.animation.AnimatorSet
|
||||||
|
import android.animation.ValueAnimator
|
||||||
|
import android.app.Activity
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.TypedValue
|
import android.util.TypedValue
|
||||||
import android.view.Gravity
|
import android.view.Gravity
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
|
import android.view.MotionEvent
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.appcompat.view.ActionMode
|
import androidx.appcompat.view.ActionMode
|
||||||
import androidx.appcompat.widget.PopupMenu
|
import androidx.appcompat.widget.PopupMenu
|
||||||
import androidx.core.math.MathUtils.clamp
|
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
@ -26,6 +32,7 @@ import eu.kanade.tachiyomi.data.database.models.LibraryManga
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
|
import eu.kanade.tachiyomi.ui.main.OnTouchEventInterface
|
||||||
import eu.kanade.tachiyomi.ui.main.SpinnerTitleInterface
|
import eu.kanade.tachiyomi.ui.main.SpinnerTitleInterface
|
||||||
import eu.kanade.tachiyomi.ui.main.SwipeGestureInterface
|
import eu.kanade.tachiyomi.ui.main.SwipeGestureInterface
|
||||||
import eu.kanade.tachiyomi.util.system.dpToPx
|
import eu.kanade.tachiyomi.util.system.dpToPx
|
||||||
@ -33,6 +40,7 @@ import eu.kanade.tachiyomi.util.system.launchUI
|
|||||||
import eu.kanade.tachiyomi.util.view.inflate
|
import eu.kanade.tachiyomi.util.view.inflate
|
||||||
import eu.kanade.tachiyomi.util.view.scrollViewWith
|
import eu.kanade.tachiyomi.util.view.scrollViewWith
|
||||||
import eu.kanade.tachiyomi.util.view.snack
|
import eu.kanade.tachiyomi.util.view.snack
|
||||||
|
import eu.kanade.tachiyomi.util.view.updateLayoutParams
|
||||||
import eu.kanade.tachiyomi.util.view.updatePaddingRelative
|
import eu.kanade.tachiyomi.util.view.updatePaddingRelative
|
||||||
import kotlinx.android.synthetic.main.filter_bottom_sheet.*
|
import kotlinx.android.synthetic.main.filter_bottom_sheet.*
|
||||||
import kotlinx.android.synthetic.main.library_grid_recycler.*
|
import kotlinx.android.synthetic.main.library_grid_recycler.*
|
||||||
@ -40,8 +48,14 @@ import kotlinx.android.synthetic.main.library_list_controller.*
|
|||||||
import kotlinx.android.synthetic.main.main_activity.*
|
import kotlinx.android.synthetic.main.main_activity.*
|
||||||
import kotlinx.android.synthetic.main.spinner_title.view.*
|
import kotlinx.android.synthetic.main.spinner_title.view.*
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import timber.log.Timber
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
import kotlin.math.abs
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
import kotlin.math.pow
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
import kotlin.math.sign
|
||||||
|
|
||||||
class LibraryListController(bundle: Bundle? = null) : LibraryController(bundle),
|
class LibraryListController(bundle: Bundle? = null) : LibraryController(bundle),
|
||||||
FlexibleAdapter.OnItemClickListener,
|
FlexibleAdapter.OnItemClickListener,
|
||||||
@ -49,6 +63,7 @@ class LibraryListController(bundle: Bundle? = null) : LibraryController(bundle),
|
|||||||
FlexibleAdapter.OnItemMoveListener,
|
FlexibleAdapter.OnItemMoveListener,
|
||||||
LibraryCategoryAdapter.LibraryListener,
|
LibraryCategoryAdapter.LibraryListener,
|
||||||
SpinnerTitleInterface,
|
SpinnerTitleInterface,
|
||||||
|
OnTouchEventInterface,
|
||||||
SwipeGestureInterface {
|
SwipeGestureInterface {
|
||||||
|
|
||||||
private lateinit var adapter: LibraryCategoryAdapter
|
private lateinit var adapter: LibraryCategoryAdapter
|
||||||
@ -66,6 +81,18 @@ class LibraryListController(bundle: Bundle? = null) : LibraryController(bundle),
|
|||||||
|
|
||||||
private var switchingCategories = false
|
private var switchingCategories = false
|
||||||
|
|
||||||
|
var startPosX:Float? = null
|
||||||
|
var startPosY:Float? = null
|
||||||
|
var moved = false
|
||||||
|
var lockedRecycler = false
|
||||||
|
var lockedY = false
|
||||||
|
var nextCategory:Int? = null
|
||||||
|
var ogCategory:Int? = null
|
||||||
|
var prevCategory:Int? = null
|
||||||
|
private val swipeDistance = 300f
|
||||||
|
var flinging = false
|
||||||
|
var isDragging = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recycler view of the list of manga.
|
* Recycler view of the list of manga.
|
||||||
*/
|
*/
|
||||||
@ -73,7 +100,7 @@ class LibraryListController(bundle: Bundle? = null) : LibraryController(bundle),
|
|||||||
|
|
||||||
override fun contentView():View = recycler_layout
|
override fun contentView():View = recycler_layout
|
||||||
|
|
||||||
/* override fun getTitle(): String? {
|
override fun getTitle(): String? {
|
||||||
return if (::customTitleSpinner.isInitialized) customTitleSpinner.category_title.text.toString()
|
return if (::customTitleSpinner.isInitialized) customTitleSpinner.category_title.text.toString()
|
||||||
else super.getTitle()
|
else super.getTitle()
|
||||||
// when {
|
// when {
|
||||||
@ -81,7 +108,7 @@ class LibraryListController(bundle: Bundle? = null) : LibraryController(bundle),
|
|||||||
// spinnerAdapter?.array?.size == 1 -> return spinnerAdapter?.array?.firstOrNull()
|
// spinnerAdapter?.array?.size == 1 -> return spinnerAdapter?.array?.firstOrNull()
|
||||||
// else -> return super.getTitle()
|
// else -> return super.getTitle()
|
||||||
// }
|
// }
|
||||||
}*/
|
}
|
||||||
|
|
||||||
private var scrollListener = object : RecyclerView.OnScrollListener () {
|
private var scrollListener = object : RecyclerView.OnScrollListener () {
|
||||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||||
@ -115,6 +142,160 @@ class LibraryListController(bundle: Bundle? = null) : LibraryController(bundle),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onTouchEvent(event: MotionEvent?) {
|
||||||
|
if (event == null) {
|
||||||
|
resetScrollingValues()
|
||||||
|
resetRecyclerY()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (flinging) return
|
||||||
|
if (isDragging) {
|
||||||
|
resetScrollingValues()
|
||||||
|
resetRecyclerY(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val sheetRect = Rect()
|
||||||
|
val recyclerRect = Rect()
|
||||||
|
bottom_sheet.getGlobalVisibleRect(sheetRect)
|
||||||
|
view?.getGlobalVisibleRect(recyclerRect)
|
||||||
|
|
||||||
|
|
||||||
|
if (startPosX == null) {
|
||||||
|
startPosX = event.rawX
|
||||||
|
startPosY = event.rawY
|
||||||
|
val position =
|
||||||
|
(recycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
|
||||||
|
val order = when (val item = adapter.getItem(position)) {
|
||||||
|
is LibraryHeaderItem -> item.category.order
|
||||||
|
is LibraryItem -> presenter.categories.find { it.id == item.manga.category }?.order
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
if (order != null) {
|
||||||
|
ogCategory = order
|
||||||
|
var newOffsetN = order + 1
|
||||||
|
while (adapter.indexOf(newOffsetN) == -1 && presenter.categories.any { it.order == newOffsetN }) {
|
||||||
|
newOffsetN += 1
|
||||||
|
}
|
||||||
|
if (adapter.indexOf(newOffsetN) != -1)
|
||||||
|
nextCategory = newOffsetN
|
||||||
|
|
||||||
|
if (position == 0) prevCategory = null
|
||||||
|
else {
|
||||||
|
var newOffsetP = order - 1
|
||||||
|
while (adapter.indexOf(newOffsetP) == -1 && presenter.categories.any { it.order == newOffsetP }) {
|
||||||
|
newOffsetP -= 1
|
||||||
|
}
|
||||||
|
if (adapter.indexOf(newOffsetP) != -1)
|
||||||
|
prevCategory = newOffsetP
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (event.actionMasked == MotionEvent.ACTION_UP) {
|
||||||
|
recycler_layout.post {
|
||||||
|
if (!flinging) {
|
||||||
|
resetScrollingValues()
|
||||||
|
resetRecyclerY(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (startPosX != null && startPosY != null &&
|
||||||
|
(sheetRect.contains(startPosX!!.toInt(), startPosY!!.toInt()) ||
|
||||||
|
!recyclerRect.contains(startPosX!!.toInt(), startPosY!!.toInt()))) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (event.actionMasked != MotionEvent.ACTION_UP && startPosX != null) {
|
||||||
|
val distance = abs(event.rawX - startPosX!!)
|
||||||
|
val sign = sign(event.rawX - startPosX!!)
|
||||||
|
|
||||||
|
if (lockedY) return
|
||||||
|
|
||||||
|
if (distance > 60 && abs(event.rawY - startPosY!!) <= 30 &&
|
||||||
|
!lockedRecycler) {
|
||||||
|
lockedRecycler = true
|
||||||
|
switchingCategories = true
|
||||||
|
recycler.suppressLayout(true)
|
||||||
|
}
|
||||||
|
else if (!lockedRecycler && abs(event.rawY - startPosY!!) > 30) {
|
||||||
|
lockedY = true
|
||||||
|
resetRecyclerY()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (abs(event.rawY - startPosY!!) <= 30 || recycler.isLayoutSuppressed
|
||||||
|
|| lockedRecycler) {
|
||||||
|
|
||||||
|
if ((prevCategory == null && sign > 0) || (nextCategory == null && sign < 0)) {
|
||||||
|
recycler_layout.x = sign * distance.pow(0.6f)
|
||||||
|
recycler_layout.alpha = 1f
|
||||||
|
}
|
||||||
|
else if (distance <= swipeDistance * 1.1f) {
|
||||||
|
recycler_layout.x = (max(0f, distance - 50f) * sign) / 3
|
||||||
|
recycler_layout.alpha =
|
||||||
|
(1f - (distance - (swipeDistance * 0.1f)) / swipeDistance)
|
||||||
|
if (moved) {
|
||||||
|
scrollToHeader(ogCategory ?: -1)
|
||||||
|
moved = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!moved) {
|
||||||
|
scrollToHeader((if (sign <= 0) nextCategory else prevCategory) ?: -1)
|
||||||
|
moved = true
|
||||||
|
}
|
||||||
|
recycler_layout.x = ((distance - swipeDistance * 2) * sign) / 3
|
||||||
|
recycler_layout.alpha = ((distance - swipeDistance * 1.1f) / swipeDistance)
|
||||||
|
if (sign > 0) {
|
||||||
|
recycler_layout.x = min(0f, recycler_layout.x)
|
||||||
|
} else {
|
||||||
|
recycler_layout.x = max(0f, recycler_layout.x)
|
||||||
|
}
|
||||||
|
recycler_layout.alpha = min(1f, recycler_layout.alpha)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resetScrollingValues() {
|
||||||
|
startPosX = null
|
||||||
|
startPosY = null
|
||||||
|
nextCategory = null
|
||||||
|
prevCategory = null
|
||||||
|
ogCategory = null
|
||||||
|
lockedY = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resetRecyclerY(animated: Boolean = false, time: Long = 100) {
|
||||||
|
moved = false
|
||||||
|
lockedRecycler = false
|
||||||
|
if (animated) {
|
||||||
|
val set = AnimatorSet()
|
||||||
|
val translationXAnimator = ValueAnimator.ofFloat(recycler_layout.x, 0f)
|
||||||
|
translationXAnimator.duration = time
|
||||||
|
translationXAnimator.addUpdateListener {
|
||||||
|
animation -> recycler_layout.x = animation.animatedValue as Float
|
||||||
|
}
|
||||||
|
|
||||||
|
val translationAlphaAnimator = ValueAnimator.ofFloat(recycler_layout.alpha, 1f)
|
||||||
|
translationAlphaAnimator.duration = time
|
||||||
|
translationAlphaAnimator.addUpdateListener {
|
||||||
|
animation -> recycler_layout.alpha = animation.animatedValue as Float
|
||||||
|
}
|
||||||
|
set.playTogether(translationXAnimator, translationAlphaAnimator)
|
||||||
|
set.start()
|
||||||
|
|
||||||
|
launchUI {
|
||||||
|
delay(time)
|
||||||
|
if (!lockedRecycler) switchingCategories = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
recycler_layout.x = 0f
|
||||||
|
recycler_layout.alpha = 1f
|
||||||
|
switchingCategories = false
|
||||||
|
}
|
||||||
|
recycler.suppressLayout(false)
|
||||||
|
}
|
||||||
|
|
||||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||||
return inflater.inflate(R.layout.library_list_controller, container, false)
|
return inflater.inflate(R.layout.library_list_controller, container, false)
|
||||||
}
|
}
|
||||||
@ -133,14 +314,13 @@ class LibraryListController(bundle: Bundle? = null) : LibraryController(bundle),
|
|||||||
})
|
})
|
||||||
recycler.setHasFixedSize(true)
|
recycler.setHasFixedSize(true)
|
||||||
recycler.adapter = adapter
|
recycler.adapter = adapter
|
||||||
adapter.fastScroller = fast_scroller
|
//adapter.fastScroller = fast_scroller
|
||||||
recycler.addOnScrollListener(scrollListener)
|
recycler.addOnScrollListener(scrollListener)
|
||||||
|
|
||||||
val tv = TypedValue()
|
val tv = TypedValue()
|
||||||
activity!!.theme.resolveAttribute(R.attr.actionBarTintColor, tv, true)
|
activity!!.theme.resolveAttribute(R.attr.actionBarTintColor, tv, true)
|
||||||
|
|
||||||
customTitleSpinner = library_layout.inflate(R.layout.spinner_title) as ViewGroup
|
customTitleSpinner = library_layout.inflate(R.layout.spinner_title) as ViewGroup
|
||||||
// (activity as MainActivity).supportActionBar?.setDisplayShowCustomEnabled(false)
|
|
||||||
spinnerAdapter = SpinnerAdapter(
|
spinnerAdapter = SpinnerAdapter(
|
||||||
view.context,
|
view.context,
|
||||||
R.layout.library_spinner_textview,
|
R.layout.library_spinner_textview,
|
||||||
@ -155,7 +335,6 @@ class LibraryListController(bundle: Bundle? = null) : LibraryController(bundle),
|
|||||||
scrollToHeader(item.itemId)
|
scrollToHeader(item.itemId)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
//(activity as MainActivity).supportActionBar?.customView = customTitleSpinner
|
|
||||||
scrollViewWith(recycler) { insets ->
|
scrollViewWith(recycler) { insets ->
|
||||||
fast_scroller.updateLayoutParams<CoordinatorLayout.LayoutParams> {
|
fast_scroller.updateLayoutParams<CoordinatorLayout.LayoutParams> {
|
||||||
topMargin = insets.systemWindowInsetTop
|
topMargin = insets.systemWindowInsetTop
|
||||||
@ -172,21 +351,13 @@ class LibraryListController(bundle: Bundle? = null) : LibraryController(bundle),
|
|||||||
activity?.toolbar?.removeSpinner()
|
activity?.toolbar?.removeSpinner()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/*if (type.isEnter) {
|
|
||||||
(activity as MainActivity).supportActionBar
|
|
||||||
?.setDisplayShowCustomEnabled(router?.backstack?.lastOrNull()?.controller() ==
|
|
||||||
this && spinnerAdapter?.array?.size ?: 0 > 1)
|
|
||||||
}
|
|
||||||
else if (type == ControllerChangeType.PUSH_EXIT) {
|
|
||||||
(activity as MainActivity).toolbar.menu.findItem(R.id
|
|
||||||
.action_search)?.collapseActionView()
|
|
||||||
(activity as MainActivity).supportActionBar?.setDisplayShowCustomEnabled(false)
|
|
||||||
}*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onActivityResumed(activity: Activity) {
|
||||||
// (activity as MainActivity).supportActionBar?.setDisplayShowCustomEnabled(false)
|
super.onActivityResumed(activity)
|
||||||
super.onDestroy()
|
if (view == null) return
|
||||||
|
resetScrollingValues()
|
||||||
|
resetRecyclerY()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNextLibraryUpdate(mangaMap: List<LibraryItem>, freshStart: Boolean) {
|
override fun onNextLibraryUpdate(mangaMap: List<LibraryItem>, freshStart: Boolean) {
|
||||||
@ -211,14 +382,12 @@ class LibraryListController(bundle: Bundle? = null) : LibraryController(bundle),
|
|||||||
|
|
||||||
val isCurrentController = router?.backstack?.lastOrNull()?.controller() ==
|
val isCurrentController = router?.backstack?.lastOrNull()?.controller() ==
|
||||||
this
|
this
|
||||||
// (activity as AppCompatActivity).supportActionBar
|
|
||||||
// ?.setDisplayShowCustomEnabled(isCurrentController && presenter.categories.size > 1)
|
|
||||||
|
|
||||||
customTitleSpinner.category_title.text =
|
/*customTitleSpinner.category_title.text =
|
||||||
presenter.categories[clamp(activeCategory,
|
presenter.categories[clamp(activeCategory,
|
||||||
0,
|
0,
|
||||||
presenter.categories.size - 1)].name
|
presenter.categories.size - 1)].name
|
||||||
if (isCurrentController) setTitle()
|
if (isCurrentController) setTitle()*/
|
||||||
updateScroll = false
|
updateScroll = false
|
||||||
if (!freshStart) {
|
if (!freshStart) {
|
||||||
justStarted = false
|
justStarted = false
|
||||||
@ -258,15 +427,35 @@ class LibraryListController(bundle: Bundle? = null) : LibraryController(bundle),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun scrollToHeader(pos: Int, fade:Boolean = false) {
|
private fun scrollToHeader(pos: Int) {
|
||||||
val headerPosition = adapter.indexOf(pos)
|
val headerPosition = adapter.indexOf(pos)
|
||||||
switchingCategories = true
|
switchingCategories = true
|
||||||
if (headerPosition > -1) {
|
if (headerPosition > -1) {
|
||||||
activity?.appbar?.y = 0f
|
val appbar = activity?.appbar
|
||||||
|
//if (headerPosition == 0)
|
||||||
|
//activity?.appbar?.y = 0f
|
||||||
recycler.suppressLayout(true)
|
recycler.suppressLayout(true)
|
||||||
|
val appbarOffset =
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
if (appbar?.y ?: 0f > -20) 0 else
|
||||||
|
(appbar?.y?.plus(view?.rootWindowInsets?.systemWindowInsetTop ?: 0)
|
||||||
|
?: 0f).roundToInt() + 10.dpToPx
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
0
|
||||||
|
}
|
||||||
(recycler.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(
|
(recycler.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(
|
||||||
headerPosition, if (headerPosition == 0) 0 else (-30).dpToPx
|
headerPosition, (if (headerPosition == 0) 0 else (-28).dpToPx)
|
||||||
|
+ appbarOffset
|
||||||
)
|
)
|
||||||
|
val isCurrentController = router?.backstack?.lastOrNull()?.controller() ==
|
||||||
|
this
|
||||||
|
|
||||||
|
val headerItem = adapter.getItem(headerPosition) as? LibraryHeaderItem
|
||||||
|
if (headerItem != null) {
|
||||||
|
customTitleSpinner.category_title.text = headerItem.category.name
|
||||||
|
if (isCurrentController) setTitle()
|
||||||
|
}
|
||||||
recycler.suppressLayout(false)
|
recycler.suppressLayout(false)
|
||||||
}
|
}
|
||||||
launchUI {
|
launchUI {
|
||||||
@ -349,6 +538,7 @@ class LibraryListController(bundle: Bundle? = null) : LibraryController(bundle),
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun startReading(position: Int) {
|
override fun startReading(position: Int) {
|
||||||
|
if (recyclerIsScrolling()) return
|
||||||
if (adapter.mode == SelectableAdapter.Mode.MULTI) {
|
if (adapter.mode == SelectableAdapter.Mode.MULTI) {
|
||||||
toggleSelection(position)
|
toggleSelection(position)
|
||||||
return
|
return
|
||||||
@ -381,7 +571,7 @@ class LibraryListController(bundle: Bundle? = null) : LibraryController(bundle),
|
|||||||
* @return true if the item should be selected, false otherwise.
|
* @return true if the item should be selected, false otherwise.
|
||||||
*/
|
*/
|
||||||
override fun onItemClick(view: View?, position: Int): Boolean {
|
override fun onItemClick(view: View?, position: Int): Boolean {
|
||||||
if (switchingCategories) return false
|
if (recyclerIsScrolling()) return false
|
||||||
val item = adapter.getItem(position) as? LibraryItem ?: return false
|
val item = adapter.getItem(position) as? LibraryItem ?: return false
|
||||||
return if (adapter.mode == SelectableAdapter.Mode.MULTI) {
|
return if (adapter.mode == SelectableAdapter.Mode.MULTI) {
|
||||||
lastClickPosition = position
|
lastClickPosition = position
|
||||||
@ -399,6 +589,7 @@ class LibraryListController(bundle: Bundle? = null) : LibraryController(bundle),
|
|||||||
* @param position the position of the element clicked.
|
* @param position the position of the element clicked.
|
||||||
*/
|
*/
|
||||||
override fun onItemLongClick(position: Int) {
|
override fun onItemLongClick(position: Int) {
|
||||||
|
if (recyclerIsScrolling()) return
|
||||||
createActionModeIfNeeded()
|
createActionModeIfNeeded()
|
||||||
when {
|
when {
|
||||||
lastClickPosition == -1 -> setSelection(position)
|
lastClickPosition == -1 -> setSelection(position)
|
||||||
@ -414,6 +605,8 @@ class LibraryListController(bundle: Bundle? = null) : LibraryController(bundle),
|
|||||||
override fun onActionStateChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
|
override fun onActionStateChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
|
||||||
val position = viewHolder?.adapterPosition ?: return
|
val position = viewHolder?.adapterPosition ?: return
|
||||||
if (actionState == 2) {
|
if (actionState == 2) {
|
||||||
|
isDragging = true
|
||||||
|
activity?.appbar?.y = 0f
|
||||||
if (lastItemPosition != null && position != lastItemPosition
|
if (lastItemPosition != null && position != lastItemPosition
|
||||||
&& lastItem == adapter.getItem(position)) {
|
&& lastItem == adapter.getItem(position)) {
|
||||||
// because for whatever reason you can repeatedly tap on a currently dragging manga
|
// because for whatever reason you can repeatedly tap on a currently dragging manga
|
||||||
@ -441,13 +634,31 @@ class LibraryListController(bundle: Bundle? = null) : LibraryController(bundle),
|
|||||||
invalidateActionMode()
|
invalidateActionMode()
|
||||||
}
|
}
|
||||||
override fun onItemMove(fromPosition: Int, toPosition: Int) {
|
override fun onItemMove(fromPosition: Int, toPosition: Int) {
|
||||||
|
// Because padding a recycler causes it to scroll up we have to scroll it back down... wild
|
||||||
|
if ((adapter.getItem(fromPosition) is LibraryItem &&
|
||||||
|
adapter.getItem(fromPosition) is LibraryItem) ||
|
||||||
|
adapter.getItem(fromPosition) == null)
|
||||||
|
recycler.scrollBy(0, recycler.paddingTop)
|
||||||
|
activity?.appbar?.y = 0f
|
||||||
if (lastItemPosition == toPosition)
|
if (lastItemPosition == toPosition)
|
||||||
lastItemPosition = null
|
lastItemPosition = null
|
||||||
else if (lastItemPosition == null)
|
else if (lastItemPosition == null)
|
||||||
lastItemPosition = fromPosition
|
lastItemPosition = fromPosition
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun shouldMoveItem(fromPosition: Int, toPosition: Int): Boolean {
|
||||||
|
if (adapter.isSelected(fromPosition))
|
||||||
|
toggleSelection(fromPosition)
|
||||||
|
val item = adapter.getItem(fromPosition) as? LibraryItem ?: return false
|
||||||
|
val newHeader = adapter.getSectionHeader(toPosition) as? LibraryHeaderItem
|
||||||
|
if (toPosition <= 1) return false
|
||||||
|
return (adapter.getItem(toPosition) !is LibraryHeaderItem)&&
|
||||||
|
(newHeader?.category?.id == item.manga.category ||
|
||||||
|
!presenter.mangaIsInCategory(item.manga, newHeader?.category?.id))
|
||||||
|
}
|
||||||
|
|
||||||
override fun onItemReleased(position: Int) {
|
override fun onItemReleased(position: Int) {
|
||||||
|
isDragging = false
|
||||||
if (adapter.selectedItemCount > 0) {
|
if (adapter.selectedItemCount > 0) {
|
||||||
lastItemPosition = null
|
lastItemPosition = null
|
||||||
return
|
return
|
||||||
@ -508,18 +719,6 @@ class LibraryListController(bundle: Bundle? = null) : LibraryController(bundle),
|
|||||||
lastItemPosition = null
|
lastItemPosition = null
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun shouldMoveItem(fromPosition: Int, toPosition: Int): Boolean {
|
|
||||||
//if (adapter.selectedItemCount > 1)
|
|
||||||
// return false
|
|
||||||
if (adapter.isSelected(fromPosition))
|
|
||||||
toggleSelection(fromPosition)
|
|
||||||
val item = adapter.getItem(fromPosition) as? LibraryItem ?: return false
|
|
||||||
val newHeader = adapter.getSectionHeader(toPosition) as? LibraryHeaderItem
|
|
||||||
//if (adapter.getItem(toPosition) is LibraryHeaderItem) return false
|
|
||||||
return newHeader?.category?.id == item.manga.category ||
|
|
||||||
!presenter.mangaIsInCategory(item.manga, newHeader?.category?.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun updateCategory(catId: Int): Boolean {
|
override fun updateCategory(catId: Int): Boolean {
|
||||||
val category = (adapter.getItem(catId) as? LibraryHeaderItem)?.category ?:
|
val category = (adapter.getItem(catId) as? LibraryHeaderItem)?.category ?:
|
||||||
return false
|
return false
|
||||||
@ -582,35 +781,53 @@ class LibraryListController(bundle: Bundle? = null) : LibraryController(bundle),
|
|||||||
if (sheetRect.contains(x.toInt(), y.toInt()))
|
if (sheetRect.contains(x.toInt(), y.toInt()))
|
||||||
showFiltersBottomSheet()
|
showFiltersBottomSheet()
|
||||||
}
|
}
|
||||||
override fun onSwipeLeft(x: Float, y: Float) = goToNextCategory(x, y,-1)
|
override fun onSwipeLeft(x: Float, y: Float) = goToNextCategory(x)
|
||||||
override fun onSwipeRight(x: Float, y: Float) = goToNextCategory(x, y,1)
|
override fun onSwipeRight(x: Float, y: Float) = goToNextCategory(x)
|
||||||
|
|
||||||
private fun goToNextCategory(x: Float, y: Float, offset: Int) {
|
private fun goToNextCategory(x: Float) {
|
||||||
val sheetRect = Rect()
|
if (lockedRecycler && abs(x) > 1000f) {
|
||||||
val recyclerRect = Rect()
|
val sign = sign(x).roundToInt()
|
||||||
bottom_sheet.getGlobalVisibleRect(sheetRect)
|
if ((sign < 0 && nextCategory == null) || (sign > 0) && prevCategory == null)
|
||||||
recycler.getGlobalVisibleRect(recyclerRect)
|
|
||||||
|
|
||||||
if (sheetRect.contains(x.toInt(), y.toInt()) ||
|
|
||||||
!recyclerRect.contains(x.toInt(), y.toInt())) {
|
|
||||||
return
|
return
|
||||||
|
val distance = recycler_layout.alpha
|
||||||
|
val speed = max(3000f / abs(x), 0.75f)
|
||||||
|
Timber.d("Flinged $distance, velo ${abs(x)}, speed $speed")
|
||||||
|
if (sign(recycler_layout.x) == sign(x)) {
|
||||||
|
flinging = true
|
||||||
|
val duration = (distance * 100 * speed).toLong()
|
||||||
|
val set = AnimatorSet()
|
||||||
|
val translationXAnimator = ValueAnimator.ofFloat(recycler_layout.x, sign * 100f)
|
||||||
|
translationXAnimator.duration = duration
|
||||||
|
translationXAnimator.addUpdateListener { animation ->
|
||||||
|
recycler_layout.x = animation.animatedValue as Float
|
||||||
}
|
}
|
||||||
val position =
|
|
||||||
(recycler.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
|
val translationAlphaAnimator = ValueAnimator.ofFloat(recycler_layout.alpha, 0f)
|
||||||
val order = when (val item = adapter.getItem(position)) {
|
translationAlphaAnimator.duration = duration
|
||||||
is LibraryHeaderItem -> item.category.order
|
translationAlphaAnimator.addUpdateListener { animation ->
|
||||||
is LibraryItem -> presenter.categories.find { it.id == item.manga.category }?.order
|
recycler_layout.alpha = animation.animatedValue as Float
|
||||||
?.plus(if (offset < 0) 1 else 0)
|
|
||||||
else -> null
|
|
||||||
}
|
}
|
||||||
if (order != null) {
|
set.playTogether(translationXAnimator, translationAlphaAnimator)
|
||||||
var newOffset = order + offset
|
set.start()
|
||||||
while (adapter.indexOf(newOffset) == -1 && presenter.categories.any { it.order == newOffset }) {
|
set.addListener(object : Animator.AnimatorListener {
|
||||||
newOffset += offset
|
override fun onAnimationEnd(animation: Animator?) {
|
||||||
|
recycler_layout.x = -sign * 100f
|
||||||
|
recycler_layout.alpha = 0f
|
||||||
|
scrollToHeader((if (sign <= 0) nextCategory else prevCategory) ?: -1)
|
||||||
|
resetScrollingValues()
|
||||||
|
resetRecyclerY(true, (100 * speed).toLong())
|
||||||
|
flinging = false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAnimationCancel(animation: Animator?) {}
|
||||||
|
|
||||||
|
override fun onAnimationRepeat(animation: Animator?) {}
|
||||||
|
|
||||||
|
override fun onAnimationStart(animation: Animator?) {}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
scrollToHeader (newOffset, true)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun popUpMenu(): PopupMenu = titlePopupMenu
|
override fun recyclerIsScrolling() = switchingCategories || lockedRecycler || lockedY
|
||||||
}
|
}
|
@ -18,6 +18,7 @@ import eu.kanade.tachiyomi.source.SourceManager
|
|||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
|
import eu.kanade.tachiyomi.ui.library.filter.FilterBottomSheet
|
||||||
import eu.kanade.tachiyomi.ui.migration.MigrationFlags
|
import eu.kanade.tachiyomi.ui.migration.MigrationFlags
|
||||||
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
||||||
import eu.kanade.tachiyomi.util.lang.removeArticles
|
import eu.kanade.tachiyomi.util.lang.removeArticles
|
||||||
@ -158,6 +159,8 @@ class LibraryPresenter(
|
|||||||
|
|
||||||
val filterMangaType by lazy { preferences.filterMangaType().getOrDefault() }
|
val filterMangaType by lazy { preferences.filterMangaType().getOrDefault() }
|
||||||
|
|
||||||
|
val filterTrackers = FilterBottomSheet.FILTER_TRACKER
|
||||||
|
|
||||||
val filterFn: (LibraryItem) -> Boolean = f@ { item ->
|
val filterFn: (LibraryItem) -> Boolean = f@ { item ->
|
||||||
// Filter when there isn't unread chapters.
|
// Filter when there isn't unread chapters.
|
||||||
if (filterUnread == STATE_INCLUDE &&
|
if (filterUnread == STATE_INCLUDE &&
|
||||||
@ -184,11 +187,18 @@ class LibraryPresenter(
|
|||||||
if (filterTracked != STATE_IGNORE) {
|
if (filterTracked != STATE_IGNORE) {
|
||||||
val tracks = db.getTracks(item.manga).executeAsBlocking()
|
val tracks = db.getTracks(item.manga).executeAsBlocking()
|
||||||
|
|
||||||
val trackCount = loggedServices.count { service ->
|
val trackCount = loggedServices.any { service ->
|
||||||
tracks.any { it.sync_id == service.id }
|
tracks.any { it.sync_id == service.id }
|
||||||
}
|
}
|
||||||
if (filterTracked == STATE_INCLUDE && trackCount == 0) return@f false
|
if (filterTracked == STATE_INCLUDE && !trackCount) return@f false
|
||||||
if (filterTracked == STATE_EXCLUDE && trackCount > 0) return@f false
|
if (filterTracked == STATE_EXCLUDE && trackCount) return@f false
|
||||||
|
|
||||||
|
if (filterTrackers.isNotEmpty()) {
|
||||||
|
val service = loggedServices.find { it.name == filterTrackers }
|
||||||
|
if (service != null) {
|
||||||
|
if (tracks.none { it.sync_id == service.id }) return@f false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Filter when there are no downloads.
|
// Filter when there are no downloads.
|
||||||
if (filterDownloaded != STATE_IGNORE) {
|
if (filterDownloaded != STATE_IGNORE) {
|
||||||
|
@ -33,7 +33,7 @@ import kotlin.math.max
|
|||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
class SortFilterBottomSheet @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null)
|
class FilterBottomSheet @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null)
|
||||||
: LinearLayout(context, attrs),
|
: LinearLayout(context, attrs),
|
||||||
FilterTagGroupListener {
|
FilterTagGroupListener {
|
||||||
|
|
||||||
@ -50,7 +50,7 @@ class SortFilterBottomSheet @JvmOverloads constructor(context: Context, attrs: A
|
|||||||
|
|
||||||
private lateinit var tracked: FilterTagGroup
|
private lateinit var tracked: FilterTagGroup
|
||||||
|
|
||||||
// private lateinit var categories: FilterTagGroup
|
private var trackers: FilterTagGroup? = null
|
||||||
|
|
||||||
private var mangaType: FilterTagGroup? = null
|
private var mangaType: FilterTagGroup? = null
|
||||||
|
|
||||||
@ -115,9 +115,6 @@ class SortFilterBottomSheet @JvmOverloads constructor(context: Context, attrs: A
|
|||||||
else
|
else
|
||||||
shadow.alpha = 1f
|
shadow.alpha = 1f
|
||||||
pager?.updatePaddingRelative(bottom = sheetBehavior?.peekHeight ?: 0)
|
pager?.updatePaddingRelative(bottom = sheetBehavior?.peekHeight ?: 0)
|
||||||
// snackbarLayout.updatePaddingRelative(bottom = sheetBehavior?.peekHeight ?: 0)
|
|
||||||
if (!phoneLandscape)
|
|
||||||
preferences.hideFiltersAtStart().set(false)
|
|
||||||
}
|
}
|
||||||
if (state == BottomSheetBehavior.STATE_EXPANDED) {
|
if (state == BottomSheetBehavior.STATE_EXPANDED) {
|
||||||
top_bar.alpha = 0f
|
top_bar.alpha = 0f
|
||||||
@ -129,8 +126,6 @@ class SortFilterBottomSheet @JvmOverloads constructor(context: Context, attrs: A
|
|||||||
shadow.alpha = 0f
|
shadow.alpha = 0f
|
||||||
pager?.updatePaddingRelative(bottom = 0)
|
pager?.updatePaddingRelative(bottom = 0)
|
||||||
// snackbarLayout.updatePaddingRelative(bottom = 0)
|
// snackbarLayout.updatePaddingRelative(bottom = 0)
|
||||||
if (!phoneLandscape)
|
|
||||||
preferences.hideFiltersAtStart().set(true)
|
|
||||||
}
|
}
|
||||||
//top_bar.isClickable = state == BottomSheetBehavior.STATE_COLLAPSED
|
//top_bar.isClickable = state == BottomSheetBehavior.STATE_COLLAPSED
|
||||||
//top_bar.isFocusable = state == BottomSheetBehavior.STATE_COLLAPSED
|
//top_bar.isFocusable = state == BottomSheetBehavior.STATE_COLLAPSED
|
||||||
@ -147,6 +142,12 @@ class SortFilterBottomSheet @JvmOverloads constructor(context: Context, attrs: A
|
|||||||
if (phoneLandscape && shadow2.visibility != View.GONE) {
|
if (phoneLandscape && shadow2.visibility != View.GONE) {
|
||||||
shadow2.gone()
|
shadow2.gone()
|
||||||
}
|
}
|
||||||
|
hide_filters.isChecked = preferences.hideFiltersAtStart().getOrDefault()
|
||||||
|
hide_filters.setOnCheckedChangeListener { _, isChecked ->
|
||||||
|
preferences.hideFiltersAtStart().set(isChecked)
|
||||||
|
if (isChecked)
|
||||||
|
onGroupClicked(ACTION_HIDE_FILTER_TIP)
|
||||||
|
}
|
||||||
createTags()
|
createTags()
|
||||||
clearButton.setOnClickListener { clearFilters() }
|
clearButton.setOnClickListener { clearFilters() }
|
||||||
}
|
}
|
||||||
@ -222,17 +223,19 @@ class SortFilterBottomSheet @JvmOverloads constructor(context: Context, attrs: A
|
|||||||
launchUI {
|
launchUI {
|
||||||
val mangaType = inflate(R.layout.filter_buttons) as FilterTagGroup
|
val mangaType = inflate(R.layout.filter_buttons) as FilterTagGroup
|
||||||
mangaType.setup(
|
mangaType.setup(
|
||||||
this@SortFilterBottomSheet,
|
this@FilterBottomSheet,
|
||||||
types.first(),
|
types.first(),
|
||||||
types.getOrNull(1),
|
types.getOrNull(1),
|
||||||
types.getOrNull(2)
|
types.getOrNull(2)
|
||||||
)
|
)
|
||||||
this@SortFilterBottomSheet.mangaType = mangaType
|
this@FilterBottomSheet.mangaType = mangaType
|
||||||
filter_layout.addView(mangaType)
|
filter_layout.addView(mangaType)
|
||||||
|
filterItems.remove(tracked)
|
||||||
filterItems.add(mangaType)
|
filterItems.add(mangaType)
|
||||||
|
filterItems.add(tracked)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
launchUI {
|
withContext(Dispatchers.Main) {
|
||||||
hide_categories.visibleIf(showCategoriesCheckBox)
|
hide_categories.visibleIf(showCategoriesCheckBox)
|
||||||
// categories.setState(preferences.hideCategories().getOrDefault())
|
// categories.setState(preferences.hideCategories().getOrDefault())
|
||||||
downloaded.setState(preferences.filterDownloaded())
|
downloaded.setState(preferences.filterDownloaded())
|
||||||
@ -243,11 +246,34 @@ class SortFilterBottomSheet @JvmOverloads constructor(context: Context, attrs: A
|
|||||||
reSortViews()
|
reSortViews()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (filterItems.contains(tracked)) {
|
||||||
|
val loggedServices = Injekt.get<TrackManager>().services.filter { it.isLogged }
|
||||||
|
if (loggedServices.size > 1) {
|
||||||
|
val serviceNames = loggedServices.map { it.name }
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
trackers = inflate(R.layout.filter_buttons) as FilterTagGroup
|
||||||
|
trackers?.setup(
|
||||||
|
this@FilterBottomSheet,
|
||||||
|
serviceNames.first(),
|
||||||
|
serviceNames.getOrNull(1),
|
||||||
|
serviceNames.getOrNull(2)
|
||||||
|
)
|
||||||
|
if (tracked.isActivated) {
|
||||||
|
filter_layout.addView(trackers)
|
||||||
|
filterItems.add(trackers!!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFilterClicked(view: FilterTagGroup, index: Int, updatePreference:Boolean) {
|
override fun onFilterClicked(view: FilterTagGroup, index: Int, updatePreference:Boolean) {
|
||||||
if (updatePreference) {
|
if (updatePreference) {
|
||||||
|
if (view == trackers) {
|
||||||
|
FILTER_TRACKER = view.nameOf(index) ?: ""
|
||||||
|
} else {
|
||||||
when (view) {
|
when (view) {
|
||||||
downloaded -> preferences.filterDownloaded()
|
downloaded -> preferences.filterDownloaded()
|
||||||
unread -> preferences.filterUnread()
|
unread -> preferences.filterUnread()
|
||||||
@ -256,8 +282,19 @@ class SortFilterBottomSheet @JvmOverloads constructor(context: Context, attrs: A
|
|||||||
mangaType -> preferences.filterMangaType()
|
mangaType -> preferences.filterMangaType()
|
||||||
else -> null
|
else -> null
|
||||||
}?.set(index + 1)
|
}?.set(index + 1)
|
||||||
|
}
|
||||||
onGroupClicked(ACTION_FILTER)
|
onGroupClicked(ACTION_FILTER)
|
||||||
}
|
}
|
||||||
|
if (preferences.filterTracked().getOrDefault() == 1 &&
|
||||||
|
trackers != null && trackers?.parent == null) {
|
||||||
|
filter_layout.addView(trackers)
|
||||||
|
filterItems.add(trackers!!)
|
||||||
|
}
|
||||||
|
else if (preferences.filterTracked().getOrDefault() != 1 &&
|
||||||
|
trackers?.parent != null) {
|
||||||
|
filter_layout.removeView(trackers)
|
||||||
|
filterItems.remove(trackers!!)
|
||||||
|
}
|
||||||
val hasFilters = hasActiveFilters()
|
val hasFilters = hasActiveFilters()
|
||||||
if (hasFilters && clearButton.parent == null)
|
if (hasFilters && clearButton.parent == null)
|
||||||
filter_layout.addView(clearButton, 0)
|
filter_layout.addView(clearButton, 0)
|
||||||
@ -275,6 +312,7 @@ class SortFilterBottomSheet @JvmOverloads constructor(context: Context, attrs: A
|
|||||||
preferences.filterCompleted().set(0)
|
preferences.filterCompleted().set(0)
|
||||||
preferences.filterTracked().set(0)
|
preferences.filterTracked().set(0)
|
||||||
preferences.filterMangaType().set(0)
|
preferences.filterMangaType().set(0)
|
||||||
|
FILTER_TRACKER = ""
|
||||||
|
|
||||||
val transition = androidx.transition.AutoTransition()
|
val transition = androidx.transition.AutoTransition()
|
||||||
transition.duration = 150
|
transition.duration = 150
|
||||||
@ -305,11 +343,9 @@ class SortFilterBottomSheet @JvmOverloads constructor(context: Context, attrs: A
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val ACTION_REFRESH = 0
|
const val ACTION_REFRESH = 0
|
||||||
const val ACTION_SORT = 1
|
const val ACTION_FILTER = 1
|
||||||
const val ACTION_FILTER = 2
|
const val ACTION_HIDE_FILTER_TIP = 2
|
||||||
const val ACTION_DISPLAY = 3
|
var FILTER_TRACKER = ""
|
||||||
const val ACTION_DOWNLOAD_BADGE = 4
|
private set
|
||||||
const val ACTION_UNREAD_BADGE = 5
|
|
||||||
const val ACTION_CAT_SORT = 6
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -30,6 +30,8 @@ class FilterTagGroup@JvmOverloads constructor(context: Context, attrs: Attribute
|
|||||||
return buttons.any { it.isActivated }
|
return buttons.any { it.isActivated }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun nameOf(index: Int):String? = buttons.getOrNull(index)?.text as? String
|
||||||
|
|
||||||
fun setup(root: ViewGroup, firstText: Int, secondText: Int? = null, thirdText: Int? = null) {
|
fun setup(root: ViewGroup, firstText: Int, secondText: Int? = null, thirdText: Int? = null) {
|
||||||
val text1 = context.getString(firstText)
|
val text1 = context.getString(firstText)
|
||||||
val text2 = if (secondText != null) context.getString(secondText) else null
|
val text2 = if (secondText != null) context.getString(secondText) else null
|
||||||
|
@ -11,6 +11,7 @@ import android.graphics.Rect
|
|||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.provider.Settings
|
||||||
import android.view.GestureDetector
|
import android.view.GestureDetector
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.MotionEvent
|
import android.view.MotionEvent
|
||||||
@ -20,7 +21,6 @@ import android.view.WindowManager
|
|||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import androidx.appcompat.content.res.AppCompatResources
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
import androidx.appcompat.graphics.drawable.DrawerArrowDrawable
|
import androidx.appcompat.graphics.drawable.DrawerArrowDrawable
|
||||||
import androidx.appcompat.widget.PopupMenu
|
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.graphics.ColorUtils
|
import androidx.core.graphics.ColorUtils
|
||||||
import androidx.core.view.GestureDetectorCompat
|
import androidx.core.view.GestureDetectorCompat
|
||||||
@ -300,7 +300,7 @@ open class MainActivity : BaseActivity(), DownloadServiceListener {
|
|||||||
.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR)
|
.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR)
|
||||||
}
|
}
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && currentNightMode == Configuration
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && currentNightMode == Configuration
|
||||||
.UI_MODE_NIGHT_NO && preferences.theme() >= 8)
|
.UI_MODE_NIGHT_NO)
|
||||||
content.systemUiVisibility = content.systemUiVisibility.or(View
|
content.systemUiVisibility = content.systemUiVisibility.or(View
|
||||||
.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR)
|
.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR)
|
||||||
|
|
||||||
@ -383,7 +383,7 @@ open class MainActivity : BaseActivity(), DownloadServiceListener {
|
|||||||
return super.startSupportActionMode(callback)
|
return super.startSupportActionMode(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* override fun onSupportActionModeFinished(mode: androidx.appcompat.view.ActionMode) {
|
override fun onSupportActionModeFinished(mode: androidx.appcompat.view.ActionMode) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) launchUI {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) launchUI {
|
||||||
val scale = Settings.Global.getFloat(
|
val scale = Settings.Global.getFloat(
|
||||||
contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1.0f
|
contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1.0f
|
||||||
@ -391,10 +391,12 @@ open class MainActivity : BaseActivity(), DownloadServiceListener {
|
|||||||
val duration = resources.getInteger(android.R.integer.config_mediumAnimTime) * scale
|
val duration = resources.getInteger(android.R.integer.config_mediumAnimTime) * scale
|
||||||
delay(duration.toLong())
|
delay(duration.toLong())
|
||||||
delay(100)
|
delay(100)
|
||||||
window?.statusBarColor = getResourceColor(android.R.attr.statusBarColor)
|
if (Color.alpha(window?.statusBarColor ?: Color.BLACK) >= 255)
|
||||||
|
window?.statusBarColor = ColorUtils.setAlphaComponent(getResourceColor(android.R.attr
|
||||||
|
.colorBackground), 175)
|
||||||
}
|
}
|
||||||
super.onSupportActionModeFinished(mode)
|
super.onSupportActionModeFinished(mode)
|
||||||
}*/
|
}
|
||||||
|
|
||||||
private fun setExtensionsBadge() {
|
private fun setExtensionsBadge() {
|
||||||
val updates = preferences.extensionUpdatesCount().getOrDefault()
|
val updates = preferences.extensionUpdatesCount().getOrDefault()
|
||||||
@ -422,7 +424,7 @@ open class MainActivity : BaseActivity(), DownloadServiceListener {
|
|||||||
GlobalScope.launch(Dispatchers.IO) {
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
val preferences: PreferencesHelper by injectLazy()
|
val preferences: PreferencesHelper by injectLazy()
|
||||||
try {
|
try {
|
||||||
val pendingUpdates = ExtensionGithubApi().checkforUpdates(this@MainActivity)
|
val pendingUpdates = ExtensionGithubApi().checkForUpdates(this@MainActivity)
|
||||||
preferences.extensionUpdatesCount().set(pendingUpdates.size)
|
preferences.extensionUpdatesCount().set(pendingUpdates.size)
|
||||||
preferences.lastExtCheck().set(Date().time)
|
preferences.lastExtCheck().set(Date().time)
|
||||||
} catch (e: java.lang.Exception) { }
|
} catch (e: java.lang.Exception) { }
|
||||||
@ -553,6 +555,9 @@ open class MainActivity : BaseActivity(), DownloadServiceListener {
|
|||||||
|
|
||||||
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
|
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
|
||||||
gestureDetector.onTouchEvent(ev)
|
gestureDetector.onTouchEvent(ev)
|
||||||
|
val controller = router.backstack.lastOrNull()?.controller()
|
||||||
|
if (controller is OnTouchEventInterface)
|
||||||
|
controller.onTouchEvent(ev)
|
||||||
if (ev?.action == MotionEvent.ACTION_DOWN) {
|
if (ev?.action == MotionEvent.ACTION_DOWN) {
|
||||||
if (snackBar != null && snackBar!!.isShown) {
|
if (snackBar != null && snackBar!!.isShown) {
|
||||||
val sRect = Rect()
|
val sRect = Rect()
|
||||||
@ -654,7 +659,7 @@ open class MainActivity : BaseActivity(), DownloadServiceListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun downloadStatusChanged(downloading: Boolean) {
|
override fun downloadStatusChanged(downloading: Boolean) {
|
||||||
val downloadManager = Injekt.get<DownloadManager>()
|
/*val downloadManager = Injekt.get<DownloadManager>()
|
||||||
val hasQueue = downloading || downloadManager.hasQueue()
|
val hasQueue = downloading || downloadManager.hasQueue()
|
||||||
launchUI {
|
launchUI {
|
||||||
if (hasQueue) {
|
if (hasQueue) {
|
||||||
@ -664,7 +669,7 @@ open class MainActivity : BaseActivity(), DownloadServiceListener {
|
|||||||
} else {
|
} else {
|
||||||
navigationView?.removeBadge(R.id.nav_library)
|
navigationView?.removeBadge(R.id.nav_library)
|
||||||
}
|
}
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -687,9 +692,9 @@ open class MainActivity : BaseActivity(), DownloadServiceListener {
|
|||||||
&& abs(diffY) <= Companion.SWIPE_THRESHOLD * 0.75f
|
&& abs(diffY) <= Companion.SWIPE_THRESHOLD * 0.75f
|
||||||
) {
|
) {
|
||||||
if (diffX > 0) {
|
if (diffX > 0) {
|
||||||
currentGestureDelegate?.onSwipeRight(e1.x, e1.y)
|
currentGestureDelegate?.onSwipeRight(velocityX, e1.y)
|
||||||
} else {
|
} else {
|
||||||
currentGestureDelegate?.onSwipeLeft(e1.x, e1.y)
|
currentGestureDelegate?.onSwipeLeft(velocityX, e1.y)
|
||||||
}
|
}
|
||||||
result = true
|
result = true
|
||||||
}
|
}
|
||||||
@ -738,9 +743,10 @@ interface BottomNavBarInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface RootSearchInterface
|
interface RootSearchInterface
|
||||||
|
interface SpinnerTitleInterface
|
||||||
|
|
||||||
interface SpinnerTitleInterface {
|
interface OnTouchEventInterface {
|
||||||
fun popUpMenu(): PopupMenu
|
fun onTouchEvent(event: MotionEvent?)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SwipeGestureInterface {
|
interface SwipeGestureInterface {
|
||||||
|
@ -47,6 +47,8 @@ class SearchActivity: MainActivity() {
|
|||||||
toolbar.navigationIcon = drawerArrow
|
toolbar.navigationIcon = drawerArrow
|
||||||
drawerArrow?.progress = 1f
|
drawerArrow?.progress = 1f
|
||||||
|
|
||||||
|
if (to !is SpinnerTitleInterface) toolbar.removeSpinner()
|
||||||
|
|
||||||
if (to is NoToolbarElevationController) {
|
if (to is NoToolbarElevationController) {
|
||||||
appbar.disableElevation()
|
appbar.disableElevation()
|
||||||
} else {
|
} else {
|
||||||
|
@ -6,17 +6,12 @@ import com.afollestad.materialdialogs.MaterialDialog
|
|||||||
import com.afollestad.materialdialogs.list.listItemsSingleChoice
|
import com.afollestad.materialdialogs.list.listItemsSingleChoice
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||||
import eu.kanade.tachiyomi.ui.manga.info.MangaInfoController
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dialog to choose a shape for the icon.
|
* Dialog to choose a shape for the icon.
|
||||||
*/
|
*/
|
||||||
class ChooseShapeDialog(bundle: Bundle? = null) : DialogController(bundle) {
|
class ChooseShapeDialog(bundle: Bundle? = null) : DialogController(bundle) {
|
||||||
|
|
||||||
constructor(target: MangaInfoController) : this() {
|
|
||||||
targetController = target
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(target: MangaDetailsController) : this() {
|
constructor(target: MangaDetailsController) : this() {
|
||||||
targetController = target
|
targetController = target
|
||||||
}
|
}
|
||||||
@ -35,7 +30,6 @@ class ChooseShapeDialog(bundle: Bundle? = null) : DialogController(bundle) {
|
|||||||
items = modes.map { activity?.getString(it) as CharSequence },
|
items = modes.map { activity?.getString(it) as CharSequence },
|
||||||
waitForPositiveButton = false)
|
waitForPositiveButton = false)
|
||||||
{ _, i, _ ->
|
{ _, i, _ ->
|
||||||
(targetController as? MangaInfoController)?.createShortcutForShape(i)
|
|
||||||
(targetController as? MangaDetailsController)?.createShortcutForShape(i)
|
(targetController as? MangaDetailsController)?.createShortcutForShape(i)
|
||||||
dismissDialog()
|
dismissDialog()
|
||||||
}
|
}
|
||||||
|
@ -1,273 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.manga
|
|
||||||
|
|
||||||
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
|
||||||
import android.app.Activity
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat
|
|
||||||
import com.bluelinelabs.conductor.ControllerChangeHandler
|
|
||||||
import com.bluelinelabs.conductor.ControllerChangeType
|
|
||||||
import com.bluelinelabs.conductor.Router
|
|
||||||
import com.bluelinelabs.conductor.RouterTransaction
|
|
||||||
import com.bluelinelabs.conductor.support.RouterPagerAdapter
|
|
||||||
import com.google.android.material.tabs.TabLayout
|
|
||||||
import com.jakewharton.rxrelay.BehaviorRelay
|
|
||||||
import com.jakewharton.rxrelay.PublishRelay
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.RxController
|
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.TabbedController
|
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
|
|
||||||
import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
|
|
||||||
import eu.kanade.tachiyomi.ui.main.BottomNavBarInterface
|
|
||||||
import eu.kanade.tachiyomi.ui.main.SearchActivity
|
|
||||||
import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersController
|
|
||||||
import eu.kanade.tachiyomi.ui.manga.info.MangaInfoController
|
|
||||||
import eu.kanade.tachiyomi.ui.manga.track.TrackController
|
|
||||||
import eu.kanade.tachiyomi.ui.migration.manga.process.MigrationListController
|
|
||||||
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
|
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
|
||||||
import eu.kanade.tachiyomi.util.view.applyWindowInsetsForController
|
|
||||||
import kotlinx.android.synthetic.main.manga_controller.*
|
|
||||||
import rx.Subscription
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import java.util.Date
|
|
||||||
|
|
||||||
class MangaController : RxController, TabbedController, BottomNavBarInterface {
|
|
||||||
|
|
||||||
constructor(manga: Manga?,
|
|
||||||
fromCatalogue: Boolean = false,
|
|
||||||
smartSearchConfig: CatalogueController.SmartSearchConfig? = null,
|
|
||||||
update: Boolean = false) : super(Bundle().apply {
|
|
||||||
putLong(MANGA_EXTRA, manga?.id ?: 0)
|
|
||||||
putBoolean(FROM_CATALOGUE_EXTRA, fromCatalogue)
|
|
||||||
putParcelable(SMART_SEARCH_CONFIG_EXTRA, smartSearchConfig)
|
|
||||||
putBoolean(UPDATE_EXTRA, update)
|
|
||||||
}) {
|
|
||||||
this.manga = manga
|
|
||||||
if (manga != null) {
|
|
||||||
source = Injekt.get<SourceManager>().getOrStub(manga.source)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(manga: Manga?, fromCatalogue: Boolean = false, fromExtension: Boolean = false) :
|
|
||||||
super
|
|
||||||
(Bundle()
|
|
||||||
.apply {
|
|
||||||
putLong(MANGA_EXTRA, manga?.id ?: 0)
|
|
||||||
putBoolean(FROM_CATALOGUE_EXTRA, fromCatalogue)
|
|
||||||
}) {
|
|
||||||
this.manga = manga
|
|
||||||
if (manga != null) {
|
|
||||||
source = Injekt.get<SourceManager>().getOrStub(manga.source)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(manga: Manga?, startY:Float?) : super(Bundle().apply {
|
|
||||||
putLong(MANGA_EXTRA, manga?.id ?: 0)
|
|
||||||
putBoolean(FROM_CATALOGUE_EXTRA, false)
|
|
||||||
}) {
|
|
||||||
this.manga = manga
|
|
||||||
startingChapterYPos = startY
|
|
||||||
if (manga != null) {
|
|
||||||
source = Injekt.get<SourceManager>().getOrStub(manga.source)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(mangaId: Long) : this(
|
|
||||||
Injekt.get<DatabaseHelper>().getManga(mangaId).executeAsBlocking())
|
|
||||||
|
|
||||||
constructor(bundle: Bundle) : this(bundle.getLong(MANGA_EXTRA)) {
|
|
||||||
val notificationId = bundle.getInt("notificationId", -1)
|
|
||||||
val context = applicationContext ?: return
|
|
||||||
if (notificationId > -1) NotificationReceiver.dismissNotification(
|
|
||||||
context, notificationId, bundle.getInt("groupId", 0)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
var manga: Manga? = null
|
|
||||||
private set
|
|
||||||
|
|
||||||
var source: Source? = null
|
|
||||||
private set
|
|
||||||
|
|
||||||
var startingChapterYPos:Float? = null
|
|
||||||
|
|
||||||
var isLockedFromSearch = false
|
|
||||||
|
|
||||||
private var adapter: MangaDetailAdapter? = null
|
|
||||||
|
|
||||||
val fromCatalogue = args.getBoolean(FROM_CATALOGUE_EXTRA, false)
|
|
||||||
|
|
||||||
val lastUpdateRelay: BehaviorRelay<Date> = BehaviorRelay.create()
|
|
||||||
|
|
||||||
val chapterCountRelay: BehaviorRelay<Float> = BehaviorRelay.create()
|
|
||||||
|
|
||||||
val mangaFavoriteRelay: PublishRelay<Boolean> = PublishRelay.create()
|
|
||||||
|
|
||||||
private val trackingIconRelay: BehaviorRelay<Boolean> = BehaviorRelay.create()
|
|
||||||
|
|
||||||
private var trackingIconSubscription: Subscription? = null
|
|
||||||
|
|
||||||
override fun getTitle(): String? {
|
|
||||||
return manga?.currentTitle()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
|
||||||
return inflater.inflate(R.layout.manga_controller, container, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View) {
|
|
||||||
super.onViewCreated(view)
|
|
||||||
view.applyWindowInsetsForController()
|
|
||||||
|
|
||||||
if (manga == null || source == null) return
|
|
||||||
|
|
||||||
requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 301)
|
|
||||||
|
|
||||||
adapter = MangaDetailAdapter()
|
|
||||||
manga_pager.offscreenPageLimit = 3
|
|
||||||
manga_pager.adapter = adapter
|
|
||||||
|
|
||||||
isLockedFromSearch = activity is SearchActivity &&
|
|
||||||
SecureActivityDelegate.shouldBeLocked()
|
|
||||||
|
|
||||||
if (!fromCatalogue)
|
|
||||||
manga_pager.currentItem = CHAPTERS_CONTROLLER
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView(view: View) {
|
|
||||||
super.onDestroyView(view)
|
|
||||||
adapter = null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActivityResumed(activity: Activity) {
|
|
||||||
super.onActivityResumed(activity)
|
|
||||||
isLockedFromSearch = activity is SearchActivity &&
|
|
||||||
SecureActivityDelegate.shouldBeLocked()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
|
|
||||||
super.onChangeStarted(handler, type)
|
|
||||||
if (type.isEnter) {
|
|
||||||
tabLayout()?.setupWithViewPager(manga_pager)
|
|
||||||
checkInitialTrackState()
|
|
||||||
trackingIconSubscription = trackingIconRelay.subscribe { setTrackingIconInternal(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun checkInitialTrackState() {
|
|
||||||
val manga = manga ?: return
|
|
||||||
val loggedServices by lazy { Injekt.get<TrackManager>().services.filter { it.isLogged } }
|
|
||||||
val db = Injekt.get<DatabaseHelper>()
|
|
||||||
val tracks = db.getTracks(manga).executeAsBlocking()
|
|
||||||
|
|
||||||
if (loggedServices.any { service -> tracks.any { it.sync_id == service.id } }) {
|
|
||||||
setTrackingIcon(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun tabLayout():TabLayout? {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateTitle(manga: Manga) {
|
|
||||||
this.manga?.title = manga.title
|
|
||||||
setTitle()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onChangeEnded(handler: ControllerChangeHandler, type: ControllerChangeType) {
|
|
||||||
super.onChangeEnded(handler, type)
|
|
||||||
if (manga == null || source == null) {
|
|
||||||
activity?.toast(R.string.manga_not_in_db)
|
|
||||||
router.popController(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun configureTabs(tabs: TabLayout) {
|
|
||||||
with(tabs) {
|
|
||||||
tabGravity = TabLayout.GRAVITY_FILL
|
|
||||||
tabMode = TabLayout.MODE_FIXED
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun cleanupTabs(tabs: TabLayout) {
|
|
||||||
trackingIconSubscription?.unsubscribe()
|
|
||||||
setTrackingIconInternal(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setTrackingIcon(visible: Boolean) {
|
|
||||||
trackingIconRelay.call(visible)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setTrackingIconInternal(visible: Boolean) {
|
|
||||||
val tab = tabLayout()?.getTabAt(TRACK_CONTROLLER) ?: return
|
|
||||||
val drawable = if (visible)
|
|
||||||
VectorDrawableCompat.create(resources!!, R.drawable.ic_done_white_18dp, null)
|
|
||||||
else null
|
|
||||||
|
|
||||||
//tab.icon = drawable
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun canChangeTabs(block: () -> Unit): Boolean {
|
|
||||||
val migrationListController = router.getControllerWithTag(MigrationListController.TAG)
|
|
||||||
as? BottomNavBarInterface
|
|
||||||
if (migrationListController != null) return migrationListController.canChangeTabs(block)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private inner class MangaDetailAdapter : RouterPagerAdapter(this@MangaController) {
|
|
||||||
|
|
||||||
private val tabCount = if (Injekt.get<TrackManager>().hasLoggedServices()) 3 else 2
|
|
||||||
|
|
||||||
private val tabTitles = listOf(
|
|
||||||
R.string.manga_detail_tab,
|
|
||||||
R.string.manga_chapters_tab,
|
|
||||||
R.string.manga_tracking_tab)
|
|
||||||
.map { resources!!.getString(it) }
|
|
||||||
|
|
||||||
override fun getCount(): Int {
|
|
||||||
return tabCount
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun configureRouter(router: Router, position: Int) {
|
|
||||||
val touchOffset = if (tabLayout()?.height == 0) 144f else 0f
|
|
||||||
if (!router.hasRootController()) {
|
|
||||||
val controller = when (position) {
|
|
||||||
INFO_CONTROLLER -> MangaInfoController()
|
|
||||||
CHAPTERS_CONTROLLER -> ChaptersController(startingChapterYPos?.minus(touchOffset))
|
|
||||||
TRACK_CONTROLLER -> TrackController()
|
|
||||||
else -> error("Wrong position $position")
|
|
||||||
}
|
|
||||||
router.setRoot(RouterTransaction.with(controller))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getPageTitle(position: Int): CharSequence {
|
|
||||||
return tabTitles[position]
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
const val UPDATE_EXTRA = "update"
|
|
||||||
const val SMART_SEARCH_CONFIG_EXTRA = "smartSearchConfig"
|
|
||||||
|
|
||||||
const val FROM_CATALOGUE_EXTRA = "from_catalogue"
|
|
||||||
const val MANGA_EXTRA = "manga"
|
|
||||||
|
|
||||||
const val INFO_CONTROLLER = 0
|
|
||||||
const val CHAPTERS_CONTROLLER = 1
|
|
||||||
const val TRACK_CONTROLLER = 2
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -27,6 +27,9 @@ import android.view.MenuItem
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.animation.DecelerateInterpolator
|
import android.view.animation.DecelerateInterpolator
|
||||||
|
import android.view.inputmethod.InputMethodManager
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.appcompat.view.ActionMode
|
||||||
import androidx.appcompat.widget.PopupMenu
|
import androidx.appcompat.widget.PopupMenu
|
||||||
import androidx.core.content.pm.ShortcutInfoCompat
|
import androidx.core.content.pm.ShortcutInfoCompat
|
||||||
import androidx.core.content.pm.ShortcutManagerCompat
|
import androidx.core.content.pm.ShortcutManagerCompat
|
||||||
@ -51,6 +54,7 @@ import com.bumptech.glide.signature.ObjectKey
|
|||||||
import com.google.android.material.snackbar.BaseTransientBottomBar
|
import com.google.android.material.snackbar.BaseTransientBottomBar
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
|
import eu.davidea.flexibleadapter.SelectableAdapter
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.Category
|
import eu.kanade.tachiyomi.data.database.models.Category
|
||||||
@ -61,23 +65,24 @@ import eu.kanade.tachiyomi.data.download.DownloadService
|
|||||||
import eu.kanade.tachiyomi.data.download.model.Download
|
import eu.kanade.tachiyomi.data.download.model.Download
|
||||||
import eu.kanade.tachiyomi.data.glide.GlideApp
|
import eu.kanade.tachiyomi.data.glide.GlideApp
|
||||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
||||||
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.BaseController
|
import eu.kanade.tachiyomi.ui.base.controller.BaseController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController
|
import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController
|
||||||
|
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
||||||
import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
|
import eu.kanade.tachiyomi.ui.catalogue.CatalogueController
|
||||||
import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
|
import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
|
||||||
import eu.kanade.tachiyomi.ui.library.LibraryController
|
import eu.kanade.tachiyomi.ui.library.LibraryController
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
import eu.kanade.tachiyomi.ui.main.SearchActivity
|
import eu.kanade.tachiyomi.ui.main.SearchActivity
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaController.Companion.FROM_CATALOGUE_EXTRA
|
|
||||||
import eu.kanade.tachiyomi.ui.manga.chapter.ChapterItem
|
import eu.kanade.tachiyomi.ui.manga.chapter.ChapterItem
|
||||||
import eu.kanade.tachiyomi.ui.manga.chapter.ChapterMatHolder
|
import eu.kanade.tachiyomi.ui.manga.chapter.ChapterMatHolder
|
||||||
import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersAdapter
|
import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersAdapter
|
||||||
import eu.kanade.tachiyomi.ui.manga.chapter.DownloadCustomChaptersDialog
|
|
||||||
import eu.kanade.tachiyomi.ui.manga.info.EditMangaDialog
|
import eu.kanade.tachiyomi.ui.manga.info.EditMangaDialog
|
||||||
|
import eu.kanade.tachiyomi.ui.manga.track.TrackItem
|
||||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||||
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
|
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
|
||||||
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
|
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
|
||||||
@ -100,22 +105,22 @@ import uy.kohesive.injekt.Injekt
|
|||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
class MangaDetailsController : BaseController,
|
open class MangaDetailsController : BaseController,
|
||||||
FlexibleAdapter.OnItemClickListener,
|
FlexibleAdapter.OnItemClickListener,
|
||||||
FlexibleAdapter.OnItemLongClickListener,
|
FlexibleAdapter.OnItemLongClickListener,
|
||||||
|
ActionMode.Callback,
|
||||||
ChaptersAdapter.MangaHeaderInterface,
|
ChaptersAdapter.MangaHeaderInterface,
|
||||||
ChangeMangaCategoriesDialog.Listener,
|
ChangeMangaCategoriesDialog.Listener,
|
||||||
DownloadCustomChaptersDialog.Listener,
|
|
||||||
NoToolbarElevationController {
|
NoToolbarElevationController {
|
||||||
|
|
||||||
constructor(manga: Manga?,
|
constructor(manga: Manga?,
|
||||||
fromCatalogue: Boolean = false,
|
fromCatalogue: Boolean = false,
|
||||||
smartSearchConfig: CatalogueController.SmartSearchConfig? = null,
|
smartSearchConfig: CatalogueController.SmartSearchConfig? = null,
|
||||||
update: Boolean = false) : super(Bundle().apply {
|
update: Boolean = false) : super(Bundle().apply {
|
||||||
putLong(MangaController.MANGA_EXTRA, manga?.id ?: 0)
|
putLong(MANGA_EXTRA, manga?.id ?: 0)
|
||||||
putBoolean(FROM_CATALOGUE_EXTRA, fromCatalogue)
|
putBoolean(FROM_CATALOGUE_EXTRA, fromCatalogue)
|
||||||
putParcelable(MangaController.SMART_SEARCH_CONFIG_EXTRA, smartSearchConfig)
|
putParcelable(SMART_SEARCH_CONFIG_EXTRA, smartSearchConfig)
|
||||||
putBoolean(MangaController.UPDATE_EXTRA, update)
|
putBoolean(UPDATE_EXTRA, update)
|
||||||
}) {
|
}) {
|
||||||
this.manga = manga
|
this.manga = manga
|
||||||
if (manga != null) {
|
if (manga != null) {
|
||||||
@ -126,7 +131,7 @@ class MangaDetailsController : BaseController,
|
|||||||
constructor(mangaId: Long) : this(
|
constructor(mangaId: Long) : this(
|
||||||
Injekt.get<DatabaseHelper>().getManga(mangaId).executeAsBlocking())
|
Injekt.get<DatabaseHelper>().getManga(mangaId).executeAsBlocking())
|
||||||
|
|
||||||
constructor(bundle: Bundle) : this(bundle.getLong(MangaController.MANGA_EXTRA)) {
|
constructor(bundle: Bundle) : this(bundle.getLong(MANGA_EXTRA)) {
|
||||||
val notificationId = bundle.getInt("notificationId", -1)
|
val notificationId = bundle.getInt("notificationId", -1)
|
||||||
val context = applicationContext ?: return
|
val context = applicationContext ?: return
|
||||||
if (notificationId > -1) NotificationReceiver.dismissNotification(
|
if (notificationId > -1) NotificationReceiver.dismissNotification(
|
||||||
@ -143,11 +148,19 @@ class MangaDetailsController : BaseController,
|
|||||||
private var snack: Snackbar? = null
|
private var snack: Snackbar? = null
|
||||||
val fromCatalogue = args.getBoolean(FROM_CATALOGUE_EXTRA, false)
|
val fromCatalogue = args.getBoolean(FROM_CATALOGUE_EXTRA, false)
|
||||||
var coverDrawable:Drawable? = null
|
var coverDrawable:Drawable? = null
|
||||||
|
var trackingBottomSheet: TrackingBottomSheet? = null
|
||||||
|
|
||||||
|
var startingDLChapterPos:Int? = null
|
||||||
/**
|
/**
|
||||||
* Adapter containing a list of chapters.
|
* Adapter containing a list of chapters.
|
||||||
*/
|
*/
|
||||||
private var adapter: ChaptersAdapter? = null
|
private var adapter: ChaptersAdapter? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action mode for selections.
|
||||||
|
*/
|
||||||
|
private var actionMode: ActionMode? = null
|
||||||
|
|
||||||
// Hold a reference to the current animator,
|
// Hold a reference to the current animator,
|
||||||
// so that it can be canceled mid-way.
|
// so that it can be canceled mid-way.
|
||||||
private var currentAnimator: Animator? = null
|
private var currentAnimator: Animator? = null
|
||||||
@ -207,6 +220,13 @@ class MangaDetailsController : BaseController,
|
|||||||
val atTop = !recycler.canScrollVertically(-1)
|
val atTop = !recycler.canScrollVertically(-1)
|
||||||
if ((!atTop && !toolbarIsColored) || (atTop && toolbarIsColored)) {
|
if ((!atTop && !toolbarIsColored) || (atTop && toolbarIsColored)) {
|
||||||
toolbarIsColored = !atTop
|
toolbarIsColored = !atTop
|
||||||
|
val isCurrentController =
|
||||||
|
router?.backstack?.lastOrNull()?.controller() == this@MangaDetailsController
|
||||||
|
if (isCurrentController) setTitle()
|
||||||
|
if (actionMode != null) {
|
||||||
|
(activity as MainActivity).toolbar.setBackgroundColor(Color.TRANSPARENT)
|
||||||
|
return
|
||||||
|
}
|
||||||
val color =
|
val color =
|
||||||
coverColor ?: activity!!.getResourceColor(android.R.attr.colorPrimary)
|
coverColor ?: activity!!.getResourceColor(android.R.attr.colorPrimary)
|
||||||
val colorFrom =
|
val colorFrom =
|
||||||
@ -228,9 +248,6 @@ class MangaDetailsController : BaseController,
|
|||||||
activity?.window?.statusBarColor = (animator.animatedValue as Int)
|
activity?.window?.statusBarColor = (animator.animatedValue as Int)
|
||||||
}
|
}
|
||||||
colorAnimator?.start()
|
colorAnimator?.start()
|
||||||
val isCurrentController =
|
|
||||||
router?.backstack?.lastOrNull()?.controller() == this@MangaDetailsController
|
|
||||||
if (isCurrentController) setTitle()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -308,11 +325,10 @@ class MangaDetailsController : BaseController,
|
|||||||
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
|
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
|
||||||
super.onChangeStarted(handler, type)
|
super.onChangeStarted(handler, type)
|
||||||
if (type == ControllerChangeType.PUSH_ENTER || type == ControllerChangeType.POP_ENTER) {
|
if (type == ControllerChangeType.PUSH_ENTER || type == ControllerChangeType.POP_ENTER) {
|
||||||
if (type == ControllerChangeType.POP_ENTER)
|
setStatusBar()
|
||||||
return
|
|
||||||
(activity as MainActivity).appbar.setBackgroundColor(Color.TRANSPARENT)
|
(activity as MainActivity).appbar.setBackgroundColor(Color.TRANSPARENT)
|
||||||
(activity as MainActivity).toolbar.setBackgroundColor(Color.TRANSPARENT)
|
(activity as MainActivity).toolbar.setBackgroundColor(activity?.window?.statusBarColor
|
||||||
activity?.window?.statusBarColor = Color.TRANSPARENT
|
?: Color.TRANSPARENT)
|
||||||
}
|
}
|
||||||
else if (type == ControllerChangeType.PUSH_EXIT || type == ControllerChangeType.POP_EXIT) {
|
else if (type == ControllerChangeType.PUSH_EXIT || type == ControllerChangeType.POP_EXIT) {
|
||||||
if (router.backstack.lastOrNull()?.controller() is DialogController)
|
if (router.backstack.lastOrNull()?.controller() is DialogController)
|
||||||
@ -347,7 +363,6 @@ class MangaDetailsController : BaseController,
|
|||||||
activity?.invalidateOptionsMenu()
|
activity?.invalidateOptionsMenu()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun updateChapters(chapters: List<ChapterItem>) {
|
fun updateChapters(chapters: List<ChapterItem>) {
|
||||||
swipe_refresh?.isRefreshing = presenter.isLoading
|
swipe_refresh?.isRefreshing = presenter.isLoading
|
||||||
if (presenter.chapters.isEmpty() && fromCatalogue && !presenter.hasRequested) {
|
if (presenter.chapters.isEmpty() && fromCatalogue && !presenter.hasRequested) {
|
||||||
@ -363,6 +378,32 @@ class MangaDetailsController : BaseController,
|
|||||||
override fun onItemClick(view: View?, position: Int): Boolean {
|
override fun onItemClick(view: View?, position: Int): Boolean {
|
||||||
val chapter = adapter?.getItem(position)?.chapter ?: return false
|
val chapter = adapter?.getItem(position)?.chapter ?: return false
|
||||||
if (chapter.isHeader) return false
|
if (chapter.isHeader) return false
|
||||||
|
if (actionMode != null) {
|
||||||
|
if (startingDLChapterPos == null) {
|
||||||
|
adapter?.addSelection(position)
|
||||||
|
(recycler.findViewHolderForAdapterPosition(position) as? BaseFlexibleViewHolder)
|
||||||
|
?.toggleActivation()
|
||||||
|
startingDLChapterPos = position
|
||||||
|
actionMode?.invalidate()
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
val startingPosition = startingDLChapterPos ?: return false
|
||||||
|
var chapterList = listOf<ChapterItem>()
|
||||||
|
when {
|
||||||
|
startingPosition > position ->
|
||||||
|
chapterList = presenter.chapters.subList(position - 1, startingPosition)
|
||||||
|
startingPosition <= position ->
|
||||||
|
chapterList = presenter.chapters.subList(startingPosition - 1, position)
|
||||||
|
}
|
||||||
|
downloadChapters(chapterList)
|
||||||
|
adapter?.removeSelection(startingPosition)
|
||||||
|
(recycler.findViewHolderForAdapterPosition(startingPosition) as? BaseFlexibleViewHolder)
|
||||||
|
?.toggleActivation()
|
||||||
|
startingDLChapterPos = null
|
||||||
|
destroyActionModeIfNeeded()
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
openChapter(chapter)
|
openChapter(chapter)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -444,6 +485,8 @@ class MangaDetailsController : BaseController,
|
|||||||
override fun onDestroyView(view: View) {
|
override fun onDestroyView(view: View) {
|
||||||
snack?.dismiss()
|
snack?.dismiss()
|
||||||
presenter.onDestroy()
|
presenter.onDestroy()
|
||||||
|
adapter = null
|
||||||
|
trackingBottomSheet = null
|
||||||
super.onDestroyView(view)
|
super.onDestroyView(view)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -547,7 +590,7 @@ class MangaDetailsController : BaseController,
|
|||||||
R.id.download_next_5 -> presenter.getUnreadChaptersSorted().take(5)
|
R.id.download_next_5 -> presenter.getUnreadChaptersSorted().take(5)
|
||||||
R.id.download_next_10 -> presenter.getUnreadChaptersSorted().take(10)
|
R.id.download_next_10 -> presenter.getUnreadChaptersSorted().take(10)
|
||||||
R.id.download_custom -> {
|
R.id.download_custom -> {
|
||||||
showCustomDownloadDialog()
|
createActionModeIfNeeded()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
R.id.download_unread -> presenter.chapters.filter { !it.read }
|
R.id.download_unread -> presenter.chapters.filter { !it.read }
|
||||||
@ -636,7 +679,7 @@ class MangaDetailsController : BaseController,
|
|||||||
val shortcutIntent = activity.intent
|
val shortcutIntent = activity.intent
|
||||||
.setAction(MainActivity.SHORTCUT_MANGA)
|
.setAction(MainActivity.SHORTCUT_MANGA)
|
||||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||||
.putExtra(MangaController.MANGA_EXTRA, presenter.manga.id)
|
.putExtra(MANGA_EXTRA, presenter.manga.id)
|
||||||
|
|
||||||
// Check if shortcut placement is supported
|
// Check if shortcut placement is supported
|
||||||
if (ShortcutManagerCompat.isRequestPinShortcutSupported(activity)) {
|
if (ShortcutManagerCompat.isRequestPinShortcutSupported(activity)) {
|
||||||
@ -665,15 +708,9 @@ class MangaDetailsController : BaseController,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showCustomDownloadDialog() {
|
override fun startDownloadRange(position: Int) {
|
||||||
DownloadCustomChaptersDialog(this, presenter.chapters.size).showDialog(router)
|
createActionModeIfNeeded()
|
||||||
}
|
onItemClick(null, position)
|
||||||
|
|
||||||
override fun downloadCustomChapters(amount: Int) {
|
|
||||||
val chaptersToDownload = presenter.getUnreadChaptersSorted().take(amount)
|
|
||||||
if (chaptersToDownload.isNotEmpty()) {
|
|
||||||
downloadChapters(chaptersToDownload)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||||
@ -869,6 +906,91 @@ class MangaDetailsController : BaseController,
|
|||||||
return super.handleBack()
|
return super.handleBack()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun showTrackingSheet() {
|
||||||
|
trackingBottomSheet = TrackingBottomSheet(this)
|
||||||
|
trackingBottomSheet?.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refreshTracking(trackings: List<TrackItem>) {
|
||||||
|
trackingBottomSheet?.onNextTrackings(trackings)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onTrackSearchResults(results: List<TrackSearch>) {
|
||||||
|
trackingBottomSheet?.onSearchResults(results)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refreshTracker() {
|
||||||
|
(recycler.findViewHolderForAdapterPosition(0) as? MangaHeaderHolder)
|
||||||
|
?.updateTracking()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun trackRefreshDone() {
|
||||||
|
trackingBottomSheet?.onRefreshDone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun trackRefreshError(error: Exception) {
|
||||||
|
trackingBottomSheet?.onRefreshError(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun trackSearchError(error: Exception) {
|
||||||
|
trackingBottomSheet?.onSearchResultsError(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the action mode if it's not created already.
|
||||||
|
*/
|
||||||
|
private fun createActionModeIfNeeded() {
|
||||||
|
if (actionMode == null) {
|
||||||
|
actionMode = (activity as AppCompatActivity).startSupportActionMode(this)
|
||||||
|
(activity as MainActivity).toolbar.setBackgroundColor(Color.TRANSPARENT)
|
||||||
|
val view = activity?.window?.currentFocus ?: return
|
||||||
|
val imm = activity?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
||||||
|
?: return
|
||||||
|
imm.hideSoftInputFromWindow(view.windowToken, 0)
|
||||||
|
if (adapter?.mode != SelectableAdapter.Mode.MULTI) {
|
||||||
|
adapter?.mode = SelectableAdapter.Mode.MULTI
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroys the action mode.
|
||||||
|
*/
|
||||||
|
private fun destroyActionModeIfNeeded() {
|
||||||
|
actionMode?.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyActionMode(mode: ActionMode?) {
|
||||||
|
actionMode = null
|
||||||
|
setStatusBar()
|
||||||
|
startingDLChapterPos = null
|
||||||
|
adapter?.mode = SelectableAdapter.Mode.IDLE
|
||||||
|
adapter?.clearSelection()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setStatusBar() {
|
||||||
|
activity?.window?.statusBarColor = if (toolbarIsColored) {
|
||||||
|
val translucentColor = ColorUtils.setAlphaComponent(coverColor ?: Color.TRANSPARENT, 175)
|
||||||
|
(activity as MainActivity).toolbar.setBackgroundColor(translucentColor)
|
||||||
|
translucentColor
|
||||||
|
} else Color.TRANSPARENT
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean {
|
||||||
|
mode?.title = view?.context?.getString(if (startingDLChapterPos == null)
|
||||||
|
R.string.select_start_chapter else R.string.select_end_chapter)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
override fun zoomImageFromThumb(thumbView: View) {
|
override fun zoomImageFromThumb(thumbView: View) {
|
||||||
// If there's an animation in progress, cancel it immediately and proceed with this one.
|
// If there's an animation in progress, cancel it immediately and proceed with this one.
|
||||||
currentAnimator?.cancel()
|
currentAnimator?.cancel()
|
||||||
@ -998,4 +1120,13 @@ class MangaDetailsController : BaseController,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
const val UPDATE_EXTRA = "update"
|
||||||
|
const val SMART_SEARCH_CONFIG_EXTRA = "smartSearchConfig"
|
||||||
|
|
||||||
|
const val FROM_CATALOGUE_EXTRA = "from_catalogue"
|
||||||
|
const val MANGA_EXTRA = "manga"
|
||||||
|
}
|
||||||
}
|
}
|
@ -20,11 +20,13 @@ import eu.kanade.tachiyomi.data.library.LibraryServiceListener
|
|||||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import eu.kanade.tachiyomi.source.LocalSource
|
import eu.kanade.tachiyomi.source.LocalSource
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import eu.kanade.tachiyomi.ui.manga.chapter.ChapterItem
|
import eu.kanade.tachiyomi.ui.manga.chapter.ChapterItem
|
||||||
|
import eu.kanade.tachiyomi.ui.manga.track.TrackItem
|
||||||
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
|
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
|
||||||
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
@ -62,6 +64,7 @@ class MangaDetailsPresenter(private val controller: MangaDetailsController,
|
|||||||
private val loggedServices by lazy { Injekt.get<TrackManager>().services.filter { it.isLogged } }
|
private val loggedServices by lazy { Injekt.get<TrackManager>().services.filter { it.isLogged } }
|
||||||
var tracks = emptyList<Track>()
|
var tracks = emptyList<Track>()
|
||||||
|
|
||||||
|
var trackList: List<TrackItem> = emptyList()
|
||||||
|
|
||||||
var chapters:List<ChapterItem> = emptyList()
|
var chapters:List<ChapterItem> = emptyList()
|
||||||
private set
|
private set
|
||||||
@ -73,6 +76,7 @@ class MangaDetailsPresenter(private val controller: MangaDetailsController,
|
|||||||
headerItem.isLocked = isLockedFromSearch
|
headerItem.isLocked = isLockedFromSearch
|
||||||
downloadManager.addListener(this)
|
downloadManager.addListener(this)
|
||||||
LibraryUpdateService.setListener(this)
|
LibraryUpdateService.setListener(this)
|
||||||
|
tracks = db.getTracks(manga).executeAsBlocking()
|
||||||
if (!manga.initialized) {
|
if (!manga.initialized) {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
controller.setRefresh(true)
|
controller.setRefresh(true)
|
||||||
@ -81,9 +85,9 @@ class MangaDetailsPresenter(private val controller: MangaDetailsController,
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
updateChapters()
|
updateChapters()
|
||||||
tracks = db.getTracks(manga).executeAsBlocking()
|
|
||||||
controller.updateChapters(this.chapters)
|
controller.updateChapters(this.chapters)
|
||||||
}
|
}
|
||||||
|
fetchTrackings()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onDestroy() {
|
fun onDestroy() {
|
||||||
@ -94,6 +98,7 @@ class MangaDetailsPresenter(private val controller: MangaDetailsController,
|
|||||||
fun fetchChapters() {
|
fun fetchChapters() {
|
||||||
launch {
|
launch {
|
||||||
getChapters()
|
getChapters()
|
||||||
|
refreshTracking()
|
||||||
withContext(Dispatchers.Main) { controller.updateChapters(chapters) }
|
withContext(Dispatchers.Main) { controller.updateChapters(chapters) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -161,7 +166,7 @@ class MangaDetailsPresenter(private val controller: MangaDetailsController,
|
|||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Sets the active display mode.
|
* Sets the active display mode.
|
||||||
* @param mode the mode to set.
|
* @param hide set title to hidden
|
||||||
*/
|
*/
|
||||||
fun hideTitle(hide: Boolean) {
|
fun hideTitle(hide: Boolean) {
|
||||||
manga.displayMode = if (hide) Manga.DISPLAY_NUMBER else Manga.DISPLAY_NAME
|
manga.displayMode = if (hide) Manga.DISPLAY_NUMBER else Manga.DISPLAY_NAME
|
||||||
@ -658,7 +663,124 @@ class MangaDetailsPresenter(private val controller: MangaDetailsController,
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isTracked(): Boolean {
|
fun isTracked(): Boolean = loggedServices.any { service -> tracks.any { it.sync_id == service.id } }
|
||||||
return loggedServices.any { service -> tracks.any { it.sync_id == service.id } }
|
|
||||||
|
fun hasTrackers(): Boolean = loggedServices.isNotEmpty()
|
||||||
|
|
||||||
|
|
||||||
|
// Tracking
|
||||||
|
|
||||||
|
private fun fetchTrackings() {
|
||||||
|
launch {
|
||||||
|
trackList = loggedServices.map { service ->
|
||||||
|
TrackItem(tracks.find { it.sync_id == service.id }, service)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun refreshTracking() {
|
||||||
|
tracks = withContext(Dispatchers.IO) { db.getTracks(manga).executeAsBlocking() }
|
||||||
|
trackList = loggedServices.map { service ->
|
||||||
|
TrackItem(tracks.find { it.sync_id == service.id }, service)
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Main) { controller.refreshTracking(trackList) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refreshTrackers() {
|
||||||
|
launch {
|
||||||
|
val list = trackList.filter { it.track != null }.map { item ->
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
val trackItem = try {
|
||||||
|
item.service.refresh(item.track!!)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
trackError(e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
if (trackItem != null) {
|
||||||
|
db.insertTrack(trackItem).executeAsBlocking()
|
||||||
|
trackItem
|
||||||
|
}
|
||||||
|
else
|
||||||
|
item.track
|
||||||
|
}
|
||||||
|
}
|
||||||
|
refreshTracking()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun trackSearch(query: String, service: TrackService) {
|
||||||
|
launch(Dispatchers.IO) {
|
||||||
|
val results = try {service.search(query) }
|
||||||
|
catch (e: Exception) {
|
||||||
|
withContext(Dispatchers.Main) { controller.trackSearchError(e) }
|
||||||
|
null }
|
||||||
|
if (!results.isNullOrEmpty()) {
|
||||||
|
withContext(Dispatchers.Main) { controller.onTrackSearchResults(results) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun registerTracking(item: Track?, service: TrackService) {
|
||||||
|
if (item != null) {
|
||||||
|
item.manga_id = manga.id!!
|
||||||
|
|
||||||
|
launch {
|
||||||
|
val binding = try { service.bind(item) }
|
||||||
|
catch (e: Exception) {
|
||||||
|
trackError(e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
if (binding != null) db.insertTrack(binding).executeAsBlocking() }
|
||||||
|
refreshTracking()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
launch {
|
||||||
|
withContext(Dispatchers.IO) { db.deleteTrackForManga(manga, service)
|
||||||
|
.executeAsBlocking() }
|
||||||
|
refreshTracking()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateRemote(track: Track, service: TrackService) {
|
||||||
|
launch {
|
||||||
|
val binding = try { service.update(track) }
|
||||||
|
catch (e: Exception) {
|
||||||
|
trackError(e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
if (binding != null) {
|
||||||
|
withContext(Dispatchers.IO) { db.insertTrack(binding).executeAsBlocking() }
|
||||||
|
refreshTracking()
|
||||||
|
}
|
||||||
|
else trackRefreshDone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun trackRefreshDone() {
|
||||||
|
async(Dispatchers.Main) { controller.trackRefreshDone() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun trackError(error: Exception) {
|
||||||
|
async(Dispatchers.Main) { controller.trackRefreshError(error) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setStatus(item: TrackItem, index: Int) {
|
||||||
|
val track = item.track!!
|
||||||
|
track.status = item.service.getStatusList()[index]
|
||||||
|
updateRemote(track, item.service)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setScore(item: TrackItem, index: Int) {
|
||||||
|
val track = item.track!!
|
||||||
|
track.score = item.service.indexToScore(index)
|
||||||
|
updateRemote(track, item.service)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setLastChapterRead(item: TrackItem, chapterNumber: Int) {
|
||||||
|
val track = item.track!!
|
||||||
|
track.last_chapter_read = chapterNumber
|
||||||
|
updateRemote(track, item.service)
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -32,12 +32,10 @@ class MangaHeaderHolder(
|
|||||||
startExpanded: Boolean
|
startExpanded: Boolean
|
||||||
) : MangaChapterHolder(view, adapter) {
|
) : MangaChapterHolder(view, adapter) {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
start_reading_button.setOnClickListener { adapter.coverListener?.readNextChapter() }
|
start_reading_button.setOnClickListener { adapter.coverListener.readNextChapter() }
|
||||||
top_view.updateLayoutParams<ConstraintLayout.LayoutParams> {
|
top_view.updateLayoutParams<ConstraintLayout.LayoutParams> {
|
||||||
height = adapter.coverListener?.topCoverHeight() ?: 0
|
height = adapter.coverListener.topCoverHeight()
|
||||||
}
|
}
|
||||||
more_button.setOnClickListener { expandDesc() }
|
more_button.setOnClickListener { expandDesc() }
|
||||||
manga_summary.setOnClickListener { expandDesc() }
|
manga_summary.setOnClickListener { expandDesc() }
|
||||||
@ -48,29 +46,30 @@ class MangaHeaderHolder(
|
|||||||
more_button_group.visible()
|
more_button_group.visible()
|
||||||
}
|
}
|
||||||
manga_genres_tags.setOnTagClickListener {
|
manga_genres_tags.setOnTagClickListener {
|
||||||
adapter.coverListener?.tagClicked(it)
|
adapter.coverListener.tagClicked(it)
|
||||||
}
|
}
|
||||||
filter_button.setOnClickListener { adapter.coverListener?.showChapterFilter() }
|
filter_button.setOnClickListener { adapter.coverListener.showChapterFilter() }
|
||||||
filters_text.setOnClickListener { adapter.coverListener?.showChapterFilter() }
|
filters_text.setOnClickListener { adapter.coverListener.showChapterFilter() }
|
||||||
chapters_title.setOnClickListener { adapter.coverListener?.showChapterFilter() }
|
chapters_title.setOnClickListener { adapter.coverListener.showChapterFilter() }
|
||||||
webview_button.setOnClickListener { adapter.coverListener?.openInWebView() }
|
webview_button.setOnClickListener { adapter.coverListener.openInWebView() }
|
||||||
share_button.setOnClickListener { adapter.coverListener?.prepareToShareManga() }
|
share_button.setOnClickListener { adapter.coverListener.prepareToShareManga() }
|
||||||
favorite_button.setOnClickListener {
|
favorite_button.setOnClickListener {
|
||||||
adapter.coverListener?.favoriteManga(false)
|
adapter.coverListener.favoriteManga(false)
|
||||||
}
|
}
|
||||||
favorite_button.setOnLongClickListener {
|
favorite_button.setOnLongClickListener {
|
||||||
adapter.coverListener?.favoriteManga(true)
|
adapter.coverListener.favoriteManga(true)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
manga_full_title.setOnLongClickListener {
|
manga_full_title.setOnLongClickListener {
|
||||||
adapter.coverListener?.copyToClipboard(manga_full_title.text.toString(), R.string.manga_info_full_title_label)
|
adapter.coverListener.copyToClipboard(manga_full_title.text.toString(), R.string.manga_info_full_title_label)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
manga_author.setOnLongClickListener {
|
manga_author.setOnLongClickListener {
|
||||||
adapter.coverListener?.copyToClipboard(manga_author.text.toString(), R.string.manga_info_author_label)
|
adapter.coverListener.copyToClipboard(manga_author.text.toString(), R.string.manga_info_author_label)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
manga_cover.setOnClickListener { adapter.coverListener?.zoomImageFromThumb(cover_card) }
|
manga_cover.setOnClickListener { adapter.coverListener.zoomImageFromThumb(cover_card) }
|
||||||
|
track_button.setOnClickListener { adapter.coverListener.showTrackingSheet() }
|
||||||
if (startExpanded)
|
if (startExpanded)
|
||||||
expandDesc()
|
expandDesc()
|
||||||
}
|
}
|
||||||
@ -144,6 +143,7 @@ class MangaHeaderHolder(
|
|||||||
val tracked = presenter.isTracked() && !item.isLocked
|
val tracked = presenter.isTracked() && !item.isLocked
|
||||||
|
|
||||||
with(track_button) {
|
with(track_button) {
|
||||||
|
visibleIf(presenter.hasTrackers())
|
||||||
text = itemView.context.getString(if (tracked) R.string.action_filter_tracked
|
text = itemView.context.getString(if (tracked) R.string.action_filter_tracked
|
||||||
else R.string.tracking)
|
else R.string.tracking)
|
||||||
|
|
||||||
@ -154,18 +154,24 @@ class MangaHeaderHolder(
|
|||||||
|
|
||||||
with(start_reading_button) {
|
with(start_reading_button) {
|
||||||
val nextChapter = presenter.getNextUnreadChapter()
|
val nextChapter = presenter.getNextUnreadChapter()
|
||||||
visibleIf(nextChapter != null && !item.isLocked)
|
visibleIf(presenter.chapters.isNotEmpty() && !item.isLocked)
|
||||||
|
isEnabled = (nextChapter != null)
|
||||||
if (nextChapter != null) {
|
if (nextChapter != null) {
|
||||||
val number = adapter.decimalFormat.format(nextChapter.chapter_number.toDouble())
|
val number = adapter.decimalFormat.format(nextChapter.chapter_number.toDouble())
|
||||||
text = resources.getString(
|
text = if (nextChapter.chapter_number > 0) resources.getString(
|
||||||
when {
|
if (nextChapter.last_page_read > 0) R.string.continue_reading_chapter
|
||||||
nextChapter.last_page_read > 0 && nextChapter.chapter_number <= 0 ->
|
else R.string.start_reading_chapter, number
|
||||||
R.string.continue_reading
|
|
||||||
nextChapter.chapter_number <= 0 -> R.string.start_reading
|
|
||||||
nextChapter.last_page_read > 0 -> R.string.continue_reading_chapter
|
|
||||||
else -> R.string.start_reader_chapter
|
|
||||||
}, number
|
|
||||||
)
|
)
|
||||||
|
else {
|
||||||
|
val name = nextChapter.name
|
||||||
|
resources.getString(
|
||||||
|
if (nextChapter.last_page_read > 0) R.string.continue_reading_x
|
||||||
|
else R.string.start_reading_x, name
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
text = resources.getString(R.string.all_caught_up)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -173,7 +179,7 @@ class MangaHeaderHolder(
|
|||||||
chapters_title.text = itemView.resources.getQuantityString(R.plurals.chapters, count, count)
|
chapters_title.text = itemView.resources.getQuantityString(R.plurals.chapters, count, count)
|
||||||
|
|
||||||
top_view.updateLayoutParams<ConstraintLayout.LayoutParams> {
|
top_view.updateLayoutParams<ConstraintLayout.LayoutParams> {
|
||||||
height = adapter.coverListener.topCoverHeight() ?: 0
|
height = adapter.coverListener.topCoverHeight()
|
||||||
}
|
}
|
||||||
|
|
||||||
manga_status.text = (itemView.context.getString( when (manga.status) {
|
manga_status.text = (itemView.context.getString( when (manga.status) {
|
||||||
@ -230,6 +236,19 @@ class MangaHeaderHolder(
|
|||||||
true_backdrop.setBackgroundColor(color)
|
true_backdrop.setBackgroundColor(color)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun updateTracking() {
|
||||||
|
val presenter = adapter.coverListener?.mangaPresenter() ?: return
|
||||||
|
val tracked = presenter.isTracked()
|
||||||
|
with(track_button) {
|
||||||
|
text = itemView.context.getString(if (tracked) R.string.action_filter_tracked
|
||||||
|
else R.string.tracking)
|
||||||
|
|
||||||
|
icon = ContextCompat.getDrawable(itemView.context, if (tracked) R.drawable
|
||||||
|
.ic_check_white_24dp else R.drawable.ic_sync_black_24dp)
|
||||||
|
checked(tracked)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onLongClick(view: View?): Boolean {
|
override fun onLongClick(view: View?): Boolean {
|
||||||
super.onLongClick(view)
|
super.onLongClick(view)
|
||||||
return false
|
return false
|
||||||
|
@ -0,0 +1,185 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.manga
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
|
import eu.kanade.tachiyomi.ui.manga.track.SetTrackChaptersDialog
|
||||||
|
import eu.kanade.tachiyomi.ui.manga.track.SetTrackScoreDialog
|
||||||
|
import eu.kanade.tachiyomi.ui.manga.track.SetTrackStatusDialog
|
||||||
|
import eu.kanade.tachiyomi.ui.manga.track.TrackAdapter
|
||||||
|
import eu.kanade.tachiyomi.ui.manga.track.TrackHolder
|
||||||
|
import eu.kanade.tachiyomi.ui.manga.track.TrackItem
|
||||||
|
import eu.kanade.tachiyomi.ui.manga.track.TrackSearchDialog
|
||||||
|
import eu.kanade.tachiyomi.util.system.dpToPx
|
||||||
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
|
import eu.kanade.tachiyomi.util.view.RecyclerWindowInsetsListener
|
||||||
|
import eu.kanade.tachiyomi.util.view.setEdgeToEdge
|
||||||
|
import kotlinx.android.synthetic.main.tracking_bottom_sheet.*
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
class TrackingBottomSheet(private val controller: MangaDetailsController) : BottomSheetDialog
|
||||||
|
(controller.activity!!, R.style.BottomSheetDialogTheme),
|
||||||
|
TrackAdapter.OnClickListener,
|
||||||
|
SetTrackStatusDialog.Listener,
|
||||||
|
SetTrackChaptersDialog.Listener,
|
||||||
|
SetTrackScoreDialog.Listener {
|
||||||
|
|
||||||
|
val activity = controller.activity!!
|
||||||
|
|
||||||
|
private var sheetBehavior: BottomSheetBehavior<*>
|
||||||
|
|
||||||
|
val presenter = controller.presenter
|
||||||
|
|
||||||
|
private var adapter: TrackAdapter? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
// Use activity theme for this layout
|
||||||
|
val view = activity.layoutInflater.inflate(R.layout.tracking_bottom_sheet, null)
|
||||||
|
setContentView(view)
|
||||||
|
|
||||||
|
sheetBehavior = BottomSheetBehavior.from(view.parent as ViewGroup)
|
||||||
|
setEdgeToEdge(activity, display_bottom_sheet, view, false)
|
||||||
|
val height = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||||
|
activity.window.decorView.rootWindowInsets.systemWindowInsetBottom
|
||||||
|
} else 0
|
||||||
|
sheetBehavior.peekHeight = 380.dpToPx + height
|
||||||
|
|
||||||
|
sheetBehavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
|
||||||
|
override fun onSlide(bottomSheet: View, progress: Float) { }
|
||||||
|
|
||||||
|
override fun onStateChanged(p0: View, state: Int) {
|
||||||
|
if (state == BottomSheetBehavior.STATE_EXPANDED) {
|
||||||
|
sheetBehavior.skipCollapsed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
sheetBehavior.skipCollapsed = true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the sheet is created. It initializes the listeners and values of the preferences.
|
||||||
|
*/
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
adapter = TrackAdapter(this)
|
||||||
|
track_recycler.layoutManager = LinearLayoutManager(context)
|
||||||
|
track_recycler.adapter = adapter
|
||||||
|
track_recycler.setOnApplyWindowInsetsListener(RecyclerWindowInsetsListener)
|
||||||
|
|
||||||
|
adapter?.items = presenter.trackList
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onNextTrackings(trackings: List<TrackItem>) {
|
||||||
|
onRefreshDone()
|
||||||
|
adapter?.items = trackings
|
||||||
|
controller.refreshTracker()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onSearchResults(results: List<TrackSearch>) {
|
||||||
|
getSearchDialog()?.onSearchResults(results)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onSearchResultsError(error: Throwable) {
|
||||||
|
Timber.e(error)
|
||||||
|
getSearchDialog()?.onSearchResultsError()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getSearchDialog(): TrackSearchDialog? {
|
||||||
|
return controller.router.getControllerWithTag(TAG_SEARCH_CONTROLLER) as? TrackSearchDialog
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onRefreshDone() {
|
||||||
|
for (i in adapter!!.items.indices) {
|
||||||
|
(track_recycler.findViewHolderForAdapterPosition(i) as? TrackHolder)?.setProgress(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onRefreshError(error: Throwable) {
|
||||||
|
for (i in adapter!!.items.indices) {
|
||||||
|
(track_recycler.findViewHolderForAdapterPosition(i) as? TrackHolder)?.setProgress(false)
|
||||||
|
}
|
||||||
|
activity.toast(error.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLogoClick(position: Int) {
|
||||||
|
val track = adapter?.getItem(position)?.track ?: return
|
||||||
|
|
||||||
|
if (track.tracking_url.isBlank()) {
|
||||||
|
activity.toast(R.string.url_not_set)
|
||||||
|
} else {
|
||||||
|
activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(track.tracking_url)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSetClick(position: Int) {
|
||||||
|
val item = adapter?.getItem(position) ?: return
|
||||||
|
TrackSearchDialog(this, item.service, item.track != null).showDialog(
|
||||||
|
controller.router, TAG_SEARCH_CONTROLLER)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStatusClick(position: Int) {
|
||||||
|
val item = adapter?.getItem(position) ?: return
|
||||||
|
if (item.track == null) return
|
||||||
|
|
||||||
|
SetTrackStatusDialog(this, item).showDialog(controller.router)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onChaptersClick(position: Int) {
|
||||||
|
val item = adapter?.getItem(position) ?: return
|
||||||
|
if (item.track == null) return
|
||||||
|
|
||||||
|
SetTrackChaptersDialog(this, item).showDialog(controller.router)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onScoreClick(position: Int) {
|
||||||
|
val item = adapter?.getItem(position) ?: return
|
||||||
|
if (item.track == null) return
|
||||||
|
|
||||||
|
SetTrackScoreDialog(this, item).showDialog(controller.router)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setStatus(item: TrackItem, selection: Int) {
|
||||||
|
presenter.setStatus(item, selection)
|
||||||
|
refreshItem(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refreshItem(item: TrackItem) {
|
||||||
|
refreshTrack(item.service)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refreshTrack(item: TrackService?) {
|
||||||
|
val index = adapter?.indexOf(item) ?: -1
|
||||||
|
if (index > -1 ){
|
||||||
|
(track_recycler.findViewHolderForAdapterPosition(index) as? TrackHolder)
|
||||||
|
?.setProgress(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setScore(item: TrackItem, score: Int) {
|
||||||
|
presenter.setScore(item, score)
|
||||||
|
refreshItem(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setChaptersRead(item: TrackItem, chaptersRead: Int) {
|
||||||
|
presenter.setLastChapterRead(item, chaptersRead)
|
||||||
|
refreshItem(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val TAG_SEARCH_CONTROLLER = "track_search_controller"
|
||||||
|
}
|
||||||
|
}
|
@ -1,136 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.manga.chapter
|
|
||||||
|
|
||||||
import android.view.View
|
|
||||||
import androidx.appcompat.widget.PopupMenu
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.download.model.Download
|
|
||||||
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
|
||||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
|
||||||
import eu.kanade.tachiyomi.util.view.gone
|
|
||||||
import eu.kanade.tachiyomi.util.view.invisible
|
|
||||||
import eu.kanade.tachiyomi.util.view.setVectorCompat
|
|
||||||
import eu.kanade.tachiyomi.util.view.visible
|
|
||||||
import kotlinx.android.synthetic.main.chapters_item.*
|
|
||||||
import java.util.Date
|
|
||||||
|
|
||||||
class ChapterHolder(
|
|
||||||
private val view: View,
|
|
||||||
private val adapter: ChaptersAdapter
|
|
||||||
) : BaseFlexibleViewHolder(view, adapter) {
|
|
||||||
|
|
||||||
init {
|
|
||||||
// We need to post a Runnable to show the popup to make sure that the PopupMenu is
|
|
||||||
// correctly positioned. The reason being that the view may change position before the
|
|
||||||
// PopupMenu is shown.
|
|
||||||
chapter_menu.setOnClickListener { it.post { showPopupMenu(it) } }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun bind(item: ChapterItem, manga: Manga) {
|
|
||||||
val chapter = item.chapter ?: return
|
|
||||||
val isLocked = item.isLocked
|
|
||||||
chapter_title.text = when (manga.displayMode) {
|
|
||||||
Manga.DISPLAY_NUMBER -> {
|
|
||||||
val number = adapter.decimalFormat.format(chapter.chapter_number.toDouble())
|
|
||||||
itemView.context.getString(R.string.display_mode_chapter, number)
|
|
||||||
}
|
|
||||||
else -> chapter.name
|
|
||||||
}
|
|
||||||
|
|
||||||
chapter_menu.visible()
|
|
||||||
// Set the correct drawable for dropdown and update the tint to match theme.
|
|
||||||
chapter_menu.setVectorCompat(R.drawable.ic_more_vert_black_24dp, view.context.getResourceColor(R.attr.icon_color))
|
|
||||||
|
|
||||||
if (isLocked) chapter_menu.invisible()
|
|
||||||
|
|
||||||
// Set correct text color
|
|
||||||
chapter_title.setTextColor(if (chapter.read && !isLocked)
|
|
||||||
adapter.readColor else adapter.unreadColor)
|
|
||||||
if (chapter.bookmark && !isLocked) chapter_title.setTextColor(adapter.bookmarkedColor)
|
|
||||||
|
|
||||||
if (chapter.date_upload > 0) {
|
|
||||||
chapter_date.text = adapter.dateFormat.format(Date(chapter.date_upload))
|
|
||||||
chapter_date.setTextColor(if (chapter.read) adapter.readColor else adapter.unreadColor)
|
|
||||||
} else {
|
|
||||||
chapter_date.text = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
//add scanlator if exists
|
|
||||||
chapter_scanlator.text = chapter.scanlator
|
|
||||||
//allow longer titles if there is no scanlator (most sources)
|
|
||||||
if (chapter_scanlator.text.isNullOrBlank()) {
|
|
||||||
chapter_title.maxLines = 2
|
|
||||||
chapter_scanlator.gone()
|
|
||||||
} else {
|
|
||||||
chapter_title.maxLines = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
chapter_pages.text = if (!chapter.read && chapter.last_page_read > 0 && !isLocked) {
|
|
||||||
itemView.context.getString(R.string.chapter_progress, chapter.last_page_read + 1)
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
}
|
|
||||||
|
|
||||||
notifyStatus(item.status, item.isLocked)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun notifyStatus(status: Int, locked: Boolean) = with(download_text) {
|
|
||||||
if (locked) {
|
|
||||||
text = ""
|
|
||||||
return
|
|
||||||
}
|
|
||||||
when (status) {
|
|
||||||
Download.QUEUE -> setText(R.string.chapter_queued)
|
|
||||||
Download.DOWNLOADING -> setText(R.string.chapter_downloading)
|
|
||||||
Download.DOWNLOADED -> setText(R.string.chapter_downloaded)
|
|
||||||
Download.ERROR -> setText(R.string.chapter_error)
|
|
||||||
else -> text = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showPopupMenu(view: View) {
|
|
||||||
val item = adapter.getItem(adapterPosition) ?: return
|
|
||||||
val chapter = item.chapter ?: return
|
|
||||||
|
|
||||||
if (item.isLocked) {
|
|
||||||
adapter.unlock()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Create a PopupMenu, giving it the clicked view for an anchor
|
|
||||||
val popup = PopupMenu(view.context, view)
|
|
||||||
|
|
||||||
// Inflate our menu resource into the PopupMenu's Menu
|
|
||||||
popup.menuInflater.inflate(R.menu.chapter_single, popup.menu)
|
|
||||||
|
|
||||||
|
|
||||||
// Hide download and show delete if the chapter is downloaded
|
|
||||||
if (item.isDownloaded) {
|
|
||||||
popup.menu.findItem(R.id.action_download).isVisible = false
|
|
||||||
popup.menu.findItem(R.id.action_delete).isVisible = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hide bookmark if bookmark
|
|
||||||
popup.menu.findItem(R.id.action_bookmark).isVisible = !chapter.bookmark
|
|
||||||
popup.menu.findItem(R.id.action_remove_bookmark).isVisible = chapter.bookmark
|
|
||||||
|
|
||||||
// Hide mark as unread when the chapter is unread
|
|
||||||
if (!chapter.read && chapter.last_page_read == 0) {
|
|
||||||
popup.menu.findItem(R.id.action_mark_as_unread).isVisible = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hide mark as read when the chapter is read
|
|
||||||
if (chapter.read) {
|
|
||||||
popup.menu.findItem(R.id.action_mark_as_read).isVisible = false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set a listener so we are notified if a menu item is clicked
|
|
||||||
popup.setOnMenuItemClickListener { menuItem ->
|
|
||||||
adapter.menuItemListener?.onMenuItemClick(adapterPosition, menuItem)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Finally show the PopupMenu
|
|
||||||
popup.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -41,7 +41,7 @@ class ChapterItem(val chapter: Chapter, val manga: Manga) :
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun isSelectable(): Boolean {
|
override fun isSelectable(): Boolean {
|
||||||
return chapter.isHeader
|
return !chapter.isHeader
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): MangaChapterHolder {
|
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): MangaChapterHolder {
|
||||||
|
@ -20,17 +20,17 @@ class ChapterMatHolder(
|
|||||||
) : MangaChapterHolder(view, adapter) {
|
) : MangaChapterHolder(view, adapter) {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
// We need to post a Runnable to show the popup to make sure that the PopupMenu is
|
|
||||||
// correctly positioned. The reason being that the view may change position before the
|
|
||||||
// PopupMenu is shown.
|
|
||||||
//chapter_menu.setOnClickListener { it.post { showPopupMenu(it) } }
|
|
||||||
download_button.setOnClickListener { downloadOrRemoveMenu() }
|
download_button.setOnClickListener { downloadOrRemoveMenu() }
|
||||||
|
download_button.setOnLongClickListener {
|
||||||
|
adapter.coverListener.startDownloadRange(adapterPosition)
|
||||||
|
true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun downloadOrRemoveMenu() {
|
private fun downloadOrRemoveMenu() {
|
||||||
val chapter = adapter.getItem(adapterPosition) ?: return
|
val chapter = adapter.getItem(adapterPosition) ?: return
|
||||||
if (chapter.status == Download.NOT_DOWNLOADED || chapter.status == Download.ERROR) {
|
if (chapter.status == Download.NOT_DOWNLOADED || chapter.status == Download.ERROR) {
|
||||||
adapter.coverListener?.downloadChapter(adapterPosition)
|
adapter.coverListener.downloadChapter(adapterPosition)
|
||||||
} else {
|
} else {
|
||||||
download_button.post {
|
download_button.post {
|
||||||
// Create a PopupMenu, giving it the clicked view for an anchor
|
// Create a PopupMenu, giving it the clicked view for an anchor
|
||||||
@ -46,7 +46,7 @@ class ChapterMatHolder(
|
|||||||
|
|
||||||
// Set a listener so we are notified if a menu item is clicked
|
// Set a listener so we are notified if a menu item is clicked
|
||||||
popup.setOnMenuItemClickListener { _ ->
|
popup.setOnMenuItemClickListener { _ ->
|
||||||
adapter.coverListener?.downloadChapter(adapterPosition)
|
adapter.coverListener.downloadChapter(adapterPosition)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,14 +1,13 @@
|
|||||||
package eu.kanade.tachiyomi.ui.manga.chapter
|
package eu.kanade.tachiyomi.ui.manga.chapter
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.view.MenuItem
|
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.BaseController
|
import eu.kanade.tachiyomi.ui.manga.MangaDetailsController
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaDetailsPresenter
|
import eu.kanade.tachiyomi.ui.manga.MangaDetailsPresenter
|
||||||
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
|
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
|
||||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||||
@ -18,7 +17,7 @@ import java.text.DecimalFormat
|
|||||||
import java.text.DecimalFormatSymbols
|
import java.text.DecimalFormatSymbols
|
||||||
|
|
||||||
class ChaptersAdapter(
|
class ChaptersAdapter(
|
||||||
val controller: BaseController,
|
val controller: MangaDetailsController,
|
||||||
context: Context
|
context: Context
|
||||||
) : FlexibleAdapter<ChapterItem>(null, controller, true) {
|
) : FlexibleAdapter<ChapterItem>(null, controller, true) {
|
||||||
|
|
||||||
@ -26,8 +25,7 @@ class ChaptersAdapter(
|
|||||||
|
|
||||||
var items: List<ChapterItem> = emptyList()
|
var items: List<ChapterItem> = emptyList()
|
||||||
|
|
||||||
val menuItemListener: OnMenuItemClickListener? = controller as? OnMenuItemClickListener
|
val coverListener: MangaHeaderInterface = controller
|
||||||
val coverListener: MangaHeaderInterface? = controller as? MangaHeaderInterface
|
|
||||||
|
|
||||||
val readColor = context.getResourceColor(android.R.attr.textColorHint)
|
val readColor = context.getResourceColor(android.R.attr.textColorHint)
|
||||||
|
|
||||||
@ -54,10 +52,6 @@ class ChaptersAdapter(
|
|||||||
SecureActivityDelegate.promptLockIfNeeded(activity)
|
SecureActivityDelegate.promptLockIfNeeded(activity)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface OnMenuItemClickListener {
|
|
||||||
fun onMenuItemClick(position: Int, item: MenuItem)
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MangaHeaderInterface {
|
interface MangaHeaderInterface {
|
||||||
fun coverColor(): Int?
|
fun coverColor(): Int?
|
||||||
fun mangaPresenter(): MangaDetailsPresenter
|
fun mangaPresenter(): MangaDetailsPresenter
|
||||||
@ -71,5 +65,7 @@ class ChaptersAdapter(
|
|||||||
fun favoriteManga(longPress: Boolean)
|
fun favoriteManga(longPress: Boolean)
|
||||||
fun copyToClipboard(content: String, label: Int)
|
fun copyToClipboard(content: String, label: Int)
|
||||||
fun zoomImageFromThumb(thumbView: View)
|
fun zoomImageFromThumb(thumbView: View)
|
||||||
|
fun showTrackingSheet()
|
||||||
|
fun startDownloadRange(position: Int)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,603 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.manga.chapter
|
|
||||||
|
|
||||||
import android.animation.Animator
|
|
||||||
import android.animation.AnimatorListenerAdapter
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Intent
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuInflater
|
|
||||||
import android.view.MenuItem
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.appcompat.view.ActionMode
|
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import com.google.android.material.snackbar.BaseTransientBottomBar
|
|
||||||
import com.google.android.material.snackbar.Snackbar
|
|
||||||
import com.jakewharton.rxbinding.support.v4.widget.refreshes
|
|
||||||
import com.jakewharton.rxbinding.view.clicks
|
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
|
||||||
import eu.davidea.flexibleadapter.SelectableAdapter
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.download.model.Download
|
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
|
||||||
import eu.kanade.tachiyomi.ui.main.SearchActivity
|
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
|
||||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
|
||||||
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
|
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
|
||||||
import eu.kanade.tachiyomi.util.view.doOnApplyWindowInsets
|
|
||||||
import eu.kanade.tachiyomi.util.view.getCoordinates
|
|
||||||
import eu.kanade.tachiyomi.util.view.getText
|
|
||||||
import eu.kanade.tachiyomi.util.view.marginBottom
|
|
||||||
import eu.kanade.tachiyomi.util.view.snack
|
|
||||||
import eu.kanade.tachiyomi.util.view.updateLayoutParams
|
|
||||||
import eu.kanade.tachiyomi.util.view.updatePaddingRelative
|
|
||||||
import kotlinx.android.synthetic.main.chapters_controller.*
|
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
class ChaptersController() : NucleusController<ChaptersPresenter>(),
|
|
||||||
ActionMode.Callback,
|
|
||||||
FlexibleAdapter.OnItemClickListener,
|
|
||||||
FlexibleAdapter.OnItemLongClickListener,
|
|
||||||
ChaptersAdapter.OnMenuItemClickListener,
|
|
||||||
DownloadCustomChaptersDialog.Listener,
|
|
||||||
DeleteChaptersDialog.Listener {
|
|
||||||
|
|
||||||
constructor(startY: Float?) : this() {
|
|
||||||
this.startingChapterYPos = startY
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adapter containing a list of chapters.
|
|
||||||
*/
|
|
||||||
private var adapter: ChaptersAdapter? = null
|
|
||||||
|
|
||||||
private var scrollToUnread = true
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Action mode for multiple selection.
|
|
||||||
*/
|
|
||||||
private var actionMode: ActionMode? = null
|
|
||||||
|
|
||||||
private var snack:Snackbar? = null
|
|
||||||
/**
|
|
||||||
* Selected items. Used to restore selections after a rotation.
|
|
||||||
*/
|
|
||||||
private val selectedItems = mutableSetOf<ChapterItem>()
|
|
||||||
|
|
||||||
private var lastClickPosition = -1
|
|
||||||
|
|
||||||
init {
|
|
||||||
setHasOptionsMenu(true)
|
|
||||||
setOptionsMenuHidden(true)
|
|
||||||
}
|
|
||||||
var startingChapterYPos:Float? = null
|
|
||||||
|
|
||||||
override fun createPresenter(): ChaptersPresenter {
|
|
||||||
val ctrl = parentController as MangaController
|
|
||||||
return ChaptersPresenter(ctrl.manga!!, ctrl.source!!,
|
|
||||||
ctrl.chapterCountRelay, ctrl.lastUpdateRelay, ctrl.mangaFavoriteRelay)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
|
||||||
return inflater.inflate(R.layout.chapters_controller, container, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View) {
|
|
||||||
super.onViewCreated(view)
|
|
||||||
|
|
||||||
// Init RecyclerView and adapter
|
|
||||||
adapter = ChaptersAdapter(this, view.context)
|
|
||||||
setReadingDrawable()
|
|
||||||
|
|
||||||
recycler.adapter = adapter
|
|
||||||
recycler.layoutManager = LinearLayoutManager(view.context)
|
|
||||||
recycler.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL))
|
|
||||||
recycler.setHasFixedSize(true)
|
|
||||||
adapter?.fastScroller = fast_scroller
|
|
||||||
|
|
||||||
val fabBaseMarginBottom = fab?.marginBottom ?: 0
|
|
||||||
recycler.doOnApplyWindowInsets { v, insets, _ ->
|
|
||||||
fab?.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
|
||||||
bottomMargin = fabBaseMarginBottom + insets.systemWindowInsetBottom
|
|
||||||
}
|
|
||||||
fast_scroller?.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
|
||||||
bottomMargin = insets.systemWindowInsetBottom
|
|
||||||
}
|
|
||||||
// offset the recycler by the fab's inset + some inset on top
|
|
||||||
v.updatePaddingRelative(bottom = insets.systemWindowInsetBottom +
|
|
||||||
v.context.resources.getDimensionPixelSize(R.dimen.fab_list_padding))
|
|
||||||
}
|
|
||||||
swipe_refresh.refreshes().subscribeUntilDestroy { fetchChaptersFromSource() }
|
|
||||||
|
|
||||||
fab.clicks().subscribeUntilDestroy {
|
|
||||||
if (activity is SearchActivity && presenter.isLockedFromSearch) {
|
|
||||||
SecureActivityDelegate.promptLockIfNeeded(activity)
|
|
||||||
return@subscribeUntilDestroy
|
|
||||||
}
|
|
||||||
val item = presenter.getNextUnreadChapter()
|
|
||||||
if (item != null) {
|
|
||||||
// Create animation listener
|
|
||||||
val revealAnimationListener: Animator.AnimatorListener = object : AnimatorListenerAdapter() {
|
|
||||||
override fun onAnimationStart(animation: Animator?) {
|
|
||||||
openChapter(item.chapter, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get coordinates and start animation
|
|
||||||
val coordinates = fab.getCoordinates()
|
|
||||||
if (!reveal_view.showRevealEffect(coordinates.x, coordinates.y, revealAnimationListener)) {
|
|
||||||
openChapter(item.chapter)
|
|
||||||
}
|
|
||||||
} else if (snack == null || snack?.getText() != view.context.getString(R.string.no_next_chapter)) {
|
|
||||||
snack = view.snack(R.string.no_next_chapter, Snackbar.LENGTH_LONG) {
|
|
||||||
addCallback(object : BaseTransientBottomBar.BaseCallback<Snackbar>() {
|
|
||||||
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
|
|
||||||
super.onDismissed(transientBottomBar, event)
|
|
||||||
if (snack == transientBottomBar) snack = null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView(view: View) {
|
|
||||||
adapter = null
|
|
||||||
actionMode = null
|
|
||||||
super.onDestroyView(view)
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Update FAB with correct drawable.
|
|
||||||
*
|
|
||||||
* @param isFavorite determines if manga is favorite or not.
|
|
||||||
*/
|
|
||||||
private fun setReadingDrawable() {
|
|
||||||
// Set the Favorite drawable to the correct one.
|
|
||||||
// Border drawable if false, filled drawable if true.
|
|
||||||
fab.setImageResource(
|
|
||||||
when {
|
|
||||||
(parentController as MangaController).isLockedFromSearch -> R.drawable.ic_lock_white_24dp
|
|
||||||
else -> R.drawable.ic_play_arrow_white_24dp
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActivityResumed(activity: Activity) {
|
|
||||||
super.onActivityResumed(activity)
|
|
||||||
if (view == null) return
|
|
||||||
if (activity is SearchActivity) {
|
|
||||||
presenter.updateLockStatus()
|
|
||||||
setReadingDrawable()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if animation view is visible
|
|
||||||
if (reveal_view.visibility == View.VISIBLE) {
|
|
||||||
// Show the unReveal effect
|
|
||||||
val coordinates = fab.getCoordinates()
|
|
||||||
reveal_view.hideRevealEffect(coordinates.x, coordinates.y, 1920)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
|
||||||
if (!(parentController as MangaController).isLockedFromSearch)
|
|
||||||
inflater.inflate(R.menu.chapters, menu)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
|
||||||
// Initialize menu items.
|
|
||||||
val menuFilterRead = menu.findItem(R.id.action_filter_read) ?: return
|
|
||||||
val menuFilterUnread = menu.findItem(R.id.action_filter_unread)
|
|
||||||
val menuFilterDownloaded = menu.findItem(R.id.action_filter_downloaded)
|
|
||||||
val menuFilterBookmarked = menu.findItem(R.id.action_filter_bookmarked)
|
|
||||||
|
|
||||||
// Set correct checkbox values.
|
|
||||||
menuFilterRead.isChecked = presenter.onlyRead()
|
|
||||||
menuFilterUnread.isChecked = presenter.onlyUnread()
|
|
||||||
menuFilterDownloaded.isChecked = presenter.onlyDownloaded()
|
|
||||||
menuFilterBookmarked.isChecked = presenter.onlyBookmarked()
|
|
||||||
|
|
||||||
if (presenter.onlyRead())
|
|
||||||
//Disable unread filter option if read filter is enabled.
|
|
||||||
menuFilterUnread.isEnabled = false
|
|
||||||
if (presenter.onlyUnread())
|
|
||||||
//Disable read filter option if unread filter is enabled.
|
|
||||||
menuFilterRead.isEnabled = false
|
|
||||||
|
|
||||||
// Display mode submenu
|
|
||||||
if (presenter.manga.displayMode == Manga.DISPLAY_NAME) {
|
|
||||||
menu.findItem(R.id.display_title).isChecked = true
|
|
||||||
} else {
|
|
||||||
menu.findItem(R.id.display_chapter_number).isChecked = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sorting mode submenu
|
|
||||||
if (presenter.manga.sorting == Manga.SORTING_SOURCE) {
|
|
||||||
menu.findItem(R.id.sort_by_source).isChecked = true
|
|
||||||
} else {
|
|
||||||
menu.findItem(R.id.sort_by_number).isChecked = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
|
||||||
when (item.itemId) {
|
|
||||||
R.id.display_title -> {
|
|
||||||
item.isChecked = true
|
|
||||||
setDisplayMode(Manga.DISPLAY_NAME)
|
|
||||||
}
|
|
||||||
R.id.display_chapter_number -> {
|
|
||||||
item.isChecked = true
|
|
||||||
setDisplayMode(Manga.DISPLAY_NUMBER)
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.sort_by_source -> {
|
|
||||||
item.isChecked = true
|
|
||||||
presenter.setSorting(Manga.SORTING_SOURCE)
|
|
||||||
}
|
|
||||||
R.id.sort_by_number -> {
|
|
||||||
item.isChecked = true
|
|
||||||
presenter.setSorting(Manga.SORTING_NUMBER)
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.download_next, R.id.download_next_5, R.id.download_next_10,
|
|
||||||
R.id.download_custom, R.id.download_unread, R.id.download_all
|
|
||||||
-> downloadChapters(item.itemId)
|
|
||||||
|
|
||||||
R.id.action_filter_unread -> {
|
|
||||||
item.isChecked = !item.isChecked
|
|
||||||
presenter.setUnreadFilter(item.isChecked)
|
|
||||||
activity?.invalidateOptionsMenu()
|
|
||||||
}
|
|
||||||
R.id.action_filter_read -> {
|
|
||||||
item.isChecked = !item.isChecked
|
|
||||||
presenter.setReadFilter(item.isChecked)
|
|
||||||
activity?.invalidateOptionsMenu()
|
|
||||||
}
|
|
||||||
R.id.action_filter_downloaded -> {
|
|
||||||
item.isChecked = !item.isChecked
|
|
||||||
presenter.setDownloadedFilter(item.isChecked)
|
|
||||||
}
|
|
||||||
R.id.action_filter_bookmarked -> {
|
|
||||||
item.isChecked = !item.isChecked
|
|
||||||
presenter.setBookmarkedFilter(item.isChecked)
|
|
||||||
}
|
|
||||||
R.id.action_filter_empty -> {
|
|
||||||
presenter.removeFilters()
|
|
||||||
activity?.invalidateOptionsMenu()
|
|
||||||
}
|
|
||||||
R.id.action_sort -> presenter.revertSortOrder()
|
|
||||||
else -> return super.onOptionsItemSelected(item)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onNextChapters(chapters: List<ChapterItem>) {
|
|
||||||
// If the list is empty, fetch chapters from source if the conditions are met
|
|
||||||
// We use presenter chapters instead because they are always unfiltered
|
|
||||||
if (presenter.chapters.isEmpty()) {
|
|
||||||
initialFetchChapters()
|
|
||||||
}
|
|
||||||
|
|
||||||
val adapter = adapter ?: return
|
|
||||||
adapter.updateDataSet(chapters)
|
|
||||||
|
|
||||||
if (selectedItems.isNotEmpty()) {
|
|
||||||
adapter.clearSelection() // we need to start from a clean state, index may have changed
|
|
||||||
createActionModeIfNeeded()
|
|
||||||
selectedItems.forEach { item ->
|
|
||||||
val position = adapter.indexOf(item)
|
|
||||||
if (position != -1 && !adapter.isSelected(position)) {
|
|
||||||
adapter.toggleSelection(position)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
actionMode?.invalidate()
|
|
||||||
}
|
|
||||||
scrollToUnread()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun scrollToUnread() {
|
|
||||||
if (adapter?.items.isNullOrEmpty()) return
|
|
||||||
if (scrollToUnread) {
|
|
||||||
val index = presenter.getFirstUnreadIndex() ?: return
|
|
||||||
val centerOfScreen =
|
|
||||||
if (startingChapterYPos != null) startingChapterYPos!!.toInt() - recycler.top - 96
|
|
||||||
else recycler.height / 2 - 96
|
|
||||||
(recycler.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(
|
|
||||||
index, centerOfScreen
|
|
||||||
)
|
|
||||||
}
|
|
||||||
scrollToUnread = false
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initialFetchChapters() {
|
|
||||||
// Only fetch if this view is from the catalog and it hasn't requested previously
|
|
||||||
if ((parentController as MangaController).fromCatalogue && !presenter.hasRequested) {
|
|
||||||
fetchChaptersFromSource()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun fetchChaptersFromSource() {
|
|
||||||
swipe_refresh?.isRefreshing = true
|
|
||||||
presenter.fetchChaptersFromSource()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onFetchChaptersDone() {
|
|
||||||
swipe_refresh?.isRefreshing = false
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onFetchChaptersError(error: Throwable) {
|
|
||||||
swipe_refresh?.isRefreshing = false
|
|
||||||
activity?.toast(error.message)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onChapterStatusChange(download: Download) {
|
|
||||||
getHolder(download.chapter)?.notifyStatus(download.status, presenter.isLockedFromSearch)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getHolder(chapter: Chapter): ChapterHolder? {
|
|
||||||
return recycler?.findViewHolderForItemId(chapter.id!!) as? ChapterHolder
|
|
||||||
}
|
|
||||||
|
|
||||||
fun openChapter(chapter: Chapter, hasAnimation: Boolean = false) {
|
|
||||||
val activity = activity ?: return
|
|
||||||
val intent = ReaderActivity.newIntent(activity, presenter.manga, chapter)
|
|
||||||
if (hasAnimation) {
|
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
|
|
||||||
}
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemClick(view: View?, position: Int): Boolean {
|
|
||||||
val adapter = adapter ?: return false
|
|
||||||
val item = adapter.getItem(position) ?: return false
|
|
||||||
if (actionMode != null && adapter.mode == SelectableAdapter.Mode.MULTI) {
|
|
||||||
lastClickPosition = position
|
|
||||||
toggleSelection(position)
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
openChapter(item.chapter)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemLongClick(position: Int) {
|
|
||||||
createActionModeIfNeeded()
|
|
||||||
when {
|
|
||||||
lastClickPosition == -1 -> setSelection(position)
|
|
||||||
lastClickPosition > position -> for (i in position until lastClickPosition)
|
|
||||||
setSelection(i)
|
|
||||||
lastClickPosition < position -> for (i in lastClickPosition + 1..position)
|
|
||||||
setSelection(i)
|
|
||||||
else -> setSelection(position)
|
|
||||||
}
|
|
||||||
lastClickPosition = position
|
|
||||||
adapter?.notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
// SELECTIONS & ACTION MODE
|
|
||||||
|
|
||||||
private fun toggleSelection(position: Int) {
|
|
||||||
val adapter = adapter ?: return
|
|
||||||
val item = adapter.getItem(position) ?: return
|
|
||||||
adapter.toggleSelection(position)
|
|
||||||
adapter.notifyDataSetChanged()
|
|
||||||
if (adapter.isSelected(position)) {
|
|
||||||
selectedItems.add(item)
|
|
||||||
} else {
|
|
||||||
selectedItems.remove(item)
|
|
||||||
}
|
|
||||||
actionMode?.invalidate()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setSelection(position: Int) {
|
|
||||||
val adapter = adapter ?: return
|
|
||||||
val item = adapter.getItem(position) ?: return
|
|
||||||
if (!adapter.isSelected(position)) {
|
|
||||||
adapter.toggleSelection(position)
|
|
||||||
selectedItems.add(item)
|
|
||||||
actionMode?.invalidate()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getSelectedChapters(): List<ChapterItem> {
|
|
||||||
val adapter = adapter ?: return emptyList()
|
|
||||||
return adapter.selectedPositions.mapNotNull { adapter.getItem(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createActionModeIfNeeded() {
|
|
||||||
if (actionMode == null) {
|
|
||||||
actionMode = (activity as? AppCompatActivity)?.startSupportActionMode(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun destroyActionModeIfNeeded() {
|
|
||||||
lastClickPosition = -1
|
|
||||||
actionMode?.finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
|
||||||
mode.menuInflater.inflate(R.menu.chapter_selection, menu)
|
|
||||||
adapter?.mode = SelectableAdapter.Mode.MULTI
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("StringFormatInvalid")
|
|
||||||
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
|
|
||||||
val count = adapter?.selectedItemCount ?: 0
|
|
||||||
if (count == 0) {
|
|
||||||
// Destroy action mode if there are no items selected.
|
|
||||||
destroyActionModeIfNeeded()
|
|
||||||
} else {
|
|
||||||
mode.title = resources?.getString(R.string.label_selected, count)
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
|
|
||||||
when (item.itemId) {
|
|
||||||
R.id.action_select_all -> selectAll()
|
|
||||||
R.id.action_mark_as_read -> markAsRead(getSelectedChapters())
|
|
||||||
R.id.action_mark_as_unread -> markAsUnread(getSelectedChapters())
|
|
||||||
R.id.action_download -> downloadChapters(getSelectedChapters())
|
|
||||||
R.id.action_delete -> showDeleteChaptersConfirmationDialog()
|
|
||||||
else -> return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyActionMode(mode: ActionMode) {
|
|
||||||
adapter?.mode = SelectableAdapter.Mode.SINGLE
|
|
||||||
adapter?.clearSelection()
|
|
||||||
selectedItems.clear()
|
|
||||||
actionMode = null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDetach(view: View) {
|
|
||||||
destroyActionModeIfNeeded()
|
|
||||||
super.onDetach(view)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMenuItemClick(position: Int, item: MenuItem) {
|
|
||||||
val chapter = adapter?.getItem(position) ?: return
|
|
||||||
val chapters = listOf(chapter)
|
|
||||||
|
|
||||||
when (item.itemId) {
|
|
||||||
R.id.action_download -> downloadChapters(chapters)
|
|
||||||
R.id.action_bookmark -> bookmarkChapters(chapters, true)
|
|
||||||
R.id.action_remove_bookmark -> bookmarkChapters(chapters, false)
|
|
||||||
R.id.action_delete -> deleteChapters(chapters)
|
|
||||||
R.id.action_mark_as_read -> markAsRead(chapters)
|
|
||||||
R.id.action_mark_as_unread -> markAsUnread(chapters)
|
|
||||||
R.id.action_mark_previous_as_read -> markPreviousAsRead(chapter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SELECTION MODE ACTIONS
|
|
||||||
|
|
||||||
private fun selectAll() {
|
|
||||||
val adapter = adapter ?: return
|
|
||||||
adapter.selectAll()
|
|
||||||
selectedItems.addAll(adapter.items)
|
|
||||||
actionMode?.invalidate()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun markAsRead(chapters: List<ChapterItem>) {
|
|
||||||
presenter.markChaptersRead(chapters, true)
|
|
||||||
if (presenter.preferences.removeAfterMarkedAsRead()) {
|
|
||||||
deleteChapters(chapters)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun markAsUnread(chapters: List<ChapterItem>) {
|
|
||||||
presenter.markChaptersRead(chapters, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun downloadChapters(chapters: List<ChapterItem>) {
|
|
||||||
val view = view
|
|
||||||
destroyActionModeIfNeeded()
|
|
||||||
presenter.downloadChapters(chapters)
|
|
||||||
if (view != null && !presenter.manga.favorite && (snack == null ||
|
|
||||||
snack?.getText() != view.context.getString(R.string.snack_add_to_library))) {
|
|
||||||
snack = view.snack(view.context.getString(R.string.snack_add_to_library), Snackbar.LENGTH_INDEFINITE) {
|
|
||||||
setAction(R.string.action_add) {
|
|
||||||
presenter.addToLibrary()
|
|
||||||
}
|
|
||||||
addCallback(object : BaseTransientBottomBar.BaseCallback<Snackbar>() {
|
|
||||||
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
|
|
||||||
super.onDismissed(transientBottomBar, event)
|
|
||||||
if (snack == transientBottomBar) snack = null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
(activity as? MainActivity)?.setUndoSnackBar(snack)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showDeleteChaptersConfirmationDialog() {
|
|
||||||
DeleteChaptersDialog(this).showDialog(router)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun deleteChapters() {
|
|
||||||
deleteChapters(getSelectedChapters())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun markPreviousAsRead(chapter: ChapterItem) {
|
|
||||||
val adapter = adapter ?: return
|
|
||||||
val chapters = if (presenter.sortDescending()) adapter.items.reversed() else adapter.items
|
|
||||||
val chapterPos = chapters.indexOf(chapter)
|
|
||||||
if (chapterPos != -1) {
|
|
||||||
markAsRead(chapters.take(chapterPos))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun bookmarkChapters(chapters: List<ChapterItem>, bookmarked: Boolean) {
|
|
||||||
destroyActionModeIfNeeded()
|
|
||||||
presenter.bookmarkChapters(chapters, bookmarked)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deleteChapters(chapters: List<ChapterItem>) {
|
|
||||||
destroyActionModeIfNeeded()
|
|
||||||
if (chapters.isEmpty()) return
|
|
||||||
presenter.deleteChapters(chapters)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onChaptersDeleted(chapters: List<ChapterItem>) {
|
|
||||||
//this is needed so the downloaded text gets removed from the item
|
|
||||||
chapters.forEach {
|
|
||||||
adapter?.updateItem(it)
|
|
||||||
}
|
|
||||||
adapter?.notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onChaptersDeletedError(error: Throwable) {
|
|
||||||
Timber.e(error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// OVERFLOW MENU DIALOGS
|
|
||||||
|
|
||||||
private fun setDisplayMode(id: Int) {
|
|
||||||
presenter.setDisplayMode(id)
|
|
||||||
adapter?.notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getUnreadChaptersSorted() = presenter.chapters
|
|
||||||
.filter { !it.read && it.status == Download.NOT_DOWNLOADED }
|
|
||||||
.distinctBy { it.name }
|
|
||||||
.sortedByDescending { it.source_order }
|
|
||||||
|
|
||||||
private fun downloadChapters(choice: Int) {
|
|
||||||
val chaptersToDownload = when (choice) {
|
|
||||||
R.id.download_next -> getUnreadChaptersSorted().take(1)
|
|
||||||
R.id.download_next_5 -> getUnreadChaptersSorted().take(5)
|
|
||||||
R.id.download_next_10 -> getUnreadChaptersSorted().take(10)
|
|
||||||
R.id.download_custom -> {
|
|
||||||
showCustomDownloadDialog()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
R.id.download_unread -> presenter.chapters.filter { !it.read }
|
|
||||||
R.id.download_all -> presenter.chapters
|
|
||||||
else -> emptyList()
|
|
||||||
}
|
|
||||||
if (chaptersToDownload.isNotEmpty()) {
|
|
||||||
downloadChapters(chaptersToDownload)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showCustomDownloadDialog() {
|
|
||||||
DownloadCustomChaptersDialog(this, presenter.chapters.size).showDialog(router)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun downloadCustomChapters(amount: Int) {
|
|
||||||
val chaptersToDownload = getUnreadChaptersSorted().take(amount)
|
|
||||||
if (chaptersToDownload.isNotEmpty()) {
|
|
||||||
downloadChapters(chaptersToDownload)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,443 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.manga.chapter
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import com.jakewharton.rxrelay.BehaviorRelay
|
|
||||||
import com.jakewharton.rxrelay.PublishRelay
|
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
|
||||||
import eu.kanade.tachiyomi.data.download.model.Download
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|
||||||
import eu.kanade.tachiyomi.source.LocalSource
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
|
||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
|
||||||
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
|
|
||||||
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
|
|
||||||
import eu.kanade.tachiyomi.util.lang.isNullOrUnsubscribed
|
|
||||||
import rx.Observable
|
|
||||||
import rx.Subscription
|
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
|
||||||
import rx.schedulers.Schedulers
|
|
||||||
import timber.log.Timber
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import java.util.Date
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Presenter of [ChaptersController].
|
|
||||||
*/
|
|
||||||
class ChaptersPresenter(
|
|
||||||
val manga: Manga,
|
|
||||||
val source: Source,
|
|
||||||
private val chapterCountRelay: BehaviorRelay<Float>,
|
|
||||||
private val lastUpdateRelay: BehaviorRelay<Date>,
|
|
||||||
private val mangaFavoriteRelay: PublishRelay<Boolean>,
|
|
||||||
val preferences: PreferencesHelper = Injekt.get(),
|
|
||||||
private val db: DatabaseHelper = Injekt.get(),
|
|
||||||
private val downloadManager: DownloadManager = Injekt.get()
|
|
||||||
) : BasePresenter<ChaptersController>() {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List of chapters of the manga. It's always unfiltered and unsorted.
|
|
||||||
*/
|
|
||||||
var chapters: List<ChapterItem> = emptyList()
|
|
||||||
private set
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Subject of list of chapters to allow updating the view without going to DB.
|
|
||||||
*/
|
|
||||||
val chaptersRelay: PublishRelay<List<ChapterItem>>
|
|
||||||
by lazy { PublishRelay.create<List<ChapterItem>>() }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the chapter list has been requested to the source.
|
|
||||||
*/
|
|
||||||
var hasRequested = false
|
|
||||||
private set
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Subscription to retrieve the new list of chapters from the source.
|
|
||||||
*/
|
|
||||||
private var fetchChaptersSubscription: Subscription? = null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Subscription to observe download status changes.
|
|
||||||
*/
|
|
||||||
private var observeDownloadsSubscription: Subscription? = null
|
|
||||||
|
|
||||||
var isLockedFromSearch = false
|
|
||||||
|
|
||||||
fun updateLockStatus() {
|
|
||||||
val lastCheck = isLockedFromSearch
|
|
||||||
isLockedFromSearch = SecureActivityDelegate.shouldBeLocked()
|
|
||||||
if (lastCheck && lastCheck != isLockedFromSearch) {
|
|
||||||
chapters.forEach {
|
|
||||||
it.isLocked = false
|
|
||||||
}
|
|
||||||
chaptersRelay.call(chapters)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
|
||||||
super.onCreate(savedState)
|
|
||||||
isLockedFromSearch = SecureActivityDelegate.shouldBeLocked()
|
|
||||||
|
|
||||||
// Prepare the relay.
|
|
||||||
chaptersRelay.flatMap { applyChapterFilters(it) }
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribeLatestCache(ChaptersController::onNextChapters
|
|
||||||
) { _, error -> Timber.e(error) }
|
|
||||||
|
|
||||||
// Add the subscription that retrieves the chapters from the database, keeps subscribed to
|
|
||||||
// changes, and sends the list of chapters to the relay.
|
|
||||||
add(db.getChapters(manga).asRxObservable()
|
|
||||||
.map { chapters ->
|
|
||||||
// Convert every chapter to a model.
|
|
||||||
chapters.map { it.toModel() }
|
|
||||||
}
|
|
||||||
.doOnNext { chapters ->
|
|
||||||
// Find downloaded chapters
|
|
||||||
setDownloadedChapters(chapters)
|
|
||||||
|
|
||||||
// Store the last emission
|
|
||||||
this.chapters = chapters
|
|
||||||
|
|
||||||
// Listen for download status changes
|
|
||||||
observeDownloads()
|
|
||||||
|
|
||||||
// Emit the number of chapters to the info tab.
|
|
||||||
chapterCountRelay.call(chapters.maxBy { it.chapter_number }?.chapter_number
|
|
||||||
?: 0f)
|
|
||||||
|
|
||||||
// Emit the upload date of the most recent chapter
|
|
||||||
lastUpdateRelay.call(Date(chapters.maxBy { it.date_upload }?.date_upload
|
|
||||||
?: 0))
|
|
||||||
|
|
||||||
}
|
|
||||||
.subscribe { chaptersRelay.call(it) })
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun observeDownloads() {
|
|
||||||
observeDownloadsSubscription?.let { remove(it) }
|
|
||||||
observeDownloadsSubscription = downloadManager.queue.getStatusObservable()
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.filter { download -> download.manga.id == manga.id }
|
|
||||||
.doOnNext { onDownloadStatusChange(it) }
|
|
||||||
.subscribeLatestCache(ChaptersController::onChapterStatusChange) {
|
|
||||||
_, error -> Timber.e(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a chapter from the database to an extended model, allowing to store new fields.
|
|
||||||
*/
|
|
||||||
private fun Chapter.toModel(): ChapterItem {
|
|
||||||
// Create the model object.
|
|
||||||
val model = ChapterItem(this, manga)
|
|
||||||
model.isLocked = isLockedFromSearch
|
|
||||||
|
|
||||||
// Find an active download for this chapter.
|
|
||||||
val download = downloadManager.queue.find { it.chapter.id == id }
|
|
||||||
|
|
||||||
if (download != null) {
|
|
||||||
// If there's an active download, assign it.
|
|
||||||
model.download = download
|
|
||||||
}
|
|
||||||
return model
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds and assigns the list of downloaded chapters.
|
|
||||||
*
|
|
||||||
* @param chapters the list of chapter from the database.
|
|
||||||
*/
|
|
||||||
private fun setDownloadedChapters(chapters: List<ChapterItem>) {
|
|
||||||
for (chapter in chapters) {
|
|
||||||
if (downloadManager.isChapterDownloaded(chapter, manga)) {
|
|
||||||
chapter.status = Download.DOWNLOADED
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Requests an updated list of chapters from the source.
|
|
||||||
*/
|
|
||||||
fun fetchChaptersFromSource() {
|
|
||||||
hasRequested = true
|
|
||||||
|
|
||||||
if (!fetchChaptersSubscription.isNullOrUnsubscribed()) return
|
|
||||||
fetchChaptersSubscription = Observable.defer { source.fetchChapterList(manga) }
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.map { syncChaptersWithSource(db, it, manga, source) }
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribeFirst({ view, _ ->
|
|
||||||
view.onFetchChaptersDone()
|
|
||||||
}, ChaptersController::onFetchChaptersError)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the UI after applying the filters.
|
|
||||||
*/
|
|
||||||
private fun refreshChapters() {
|
|
||||||
chaptersRelay.call(chapters)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Applies the view filters to the list of chapters obtained from the database.
|
|
||||||
* @param chapters the list of chapters from the database
|
|
||||||
* @return an observable of the list of chapters filtered and sorted.
|
|
||||||
*/
|
|
||||||
private fun applyChapterFilters(chapters: List<ChapterItem>): Observable<List<ChapterItem>> {
|
|
||||||
var observable = Observable.from(chapters).subscribeOn(Schedulers.io())
|
|
||||||
if (onlyUnread()) {
|
|
||||||
observable = observable.filter { !it.read }
|
|
||||||
} else if (onlyRead()) {
|
|
||||||
observable = observable.filter { it.read }
|
|
||||||
}
|
|
||||||
if (onlyDownloaded()) {
|
|
||||||
observable = observable.filter { it.isDownloaded || it.manga.source == LocalSource.ID }
|
|
||||||
}
|
|
||||||
if (onlyBookmarked()) {
|
|
||||||
observable = observable.filter { it.bookmark }
|
|
||||||
}
|
|
||||||
val sortFunction: (Chapter, Chapter) -> Int = when (manga.sorting) {
|
|
||||||
Manga.SORTING_SOURCE -> when (sortDescending()) {
|
|
||||||
true -> { c1, c2 -> c1.source_order.compareTo(c2.source_order) }
|
|
||||||
false -> { c1, c2 -> c2.source_order.compareTo(c1.source_order) }
|
|
||||||
}
|
|
||||||
Manga.SORTING_NUMBER -> when (sortDescending()) {
|
|
||||||
true -> { c1, c2 -> c2.chapter_number.compareTo(c1.chapter_number) }
|
|
||||||
false -> { c1, c2 -> c1.chapter_number.compareTo(c2.chapter_number) }
|
|
||||||
}
|
|
||||||
else -> throw NotImplementedError("Unimplemented sorting method")
|
|
||||||
}
|
|
||||||
return observable.toSortedList(sortFunction)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when a download for the active manga changes status.
|
|
||||||
* @param download the download whose status changed.
|
|
||||||
*/
|
|
||||||
fun onDownloadStatusChange(download: Download) {
|
|
||||||
// Assign the download to the model object.
|
|
||||||
if (download.status == Download.QUEUE) {
|
|
||||||
chapters.find { it.id == download.chapter.id }?.let {
|
|
||||||
if (it.download == null) {
|
|
||||||
it.download = download
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Force UI update if downloaded filter active and download finished.
|
|
||||||
if (onlyDownloaded() && download.status == Download.DOWNLOADED)
|
|
||||||
refreshChapters()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the next unread chapter or null if everything is read.
|
|
||||||
*/
|
|
||||||
fun getNextUnreadChapter(): ChapterItem? {
|
|
||||||
return chapters.sortedByDescending { it.source_order }.find { !it.read }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mark the selected chapter list as read/unread.
|
|
||||||
* @param selectedChapters the list of selected chapters.
|
|
||||||
* @param read whether to mark chapters as read or unread.
|
|
||||||
*/
|
|
||||||
fun markChaptersRead(selectedChapters: List<ChapterItem>, read: Boolean) {
|
|
||||||
Observable.from(selectedChapters)
|
|
||||||
.doOnNext { chapter ->
|
|
||||||
chapter.read = read
|
|
||||||
if (!read) {
|
|
||||||
chapter.last_page_read = 0
|
|
||||||
chapter.pages_left = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.toList()
|
|
||||||
.flatMap { db.updateChaptersProgress(it).asRxObservable() }
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.subscribe()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Downloads the given list of chapters with the manager.
|
|
||||||
* @param chapters the list of chapters to download.
|
|
||||||
*/
|
|
||||||
fun downloadChapters(chapters: List<ChapterItem>) {
|
|
||||||
downloadManager.downloadChapters(manga, chapters)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Bookmarks the given list of chapters.
|
|
||||||
* @param selectedChapters the list of chapters to bookmark.
|
|
||||||
*/
|
|
||||||
fun bookmarkChapters(selectedChapters: List<ChapterItem>, bookmarked: Boolean) {
|
|
||||||
Observable.from(selectedChapters)
|
|
||||||
.doOnNext { chapter ->
|
|
||||||
chapter.bookmark = bookmarked
|
|
||||||
}
|
|
||||||
.toList()
|
|
||||||
.flatMap { db.updateChaptersProgress(it).asRxObservable() }
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.subscribe()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes the given list of chapter.
|
|
||||||
* @param chapters the list of chapters to delete.
|
|
||||||
*/
|
|
||||||
fun deleteChapters(chapters: List<ChapterItem>) {
|
|
||||||
Observable.just(chapters)
|
|
||||||
.doOnNext { deleteChaptersInternal(chapters) }
|
|
||||||
.doOnNext { if (onlyDownloaded()) refreshChapters() }
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribeFirst({ view, _ ->
|
|
||||||
view.onChaptersDeleted(chapters)
|
|
||||||
}, ChaptersController::onChaptersDeletedError)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes a list of chapters from disk. This method is called in a background thread.
|
|
||||||
* @param chapters the chapters to delete.
|
|
||||||
*/
|
|
||||||
private fun deleteChaptersInternal(chapters: List<ChapterItem>) {
|
|
||||||
downloadManager.deleteChapters(chapters, manga, source)
|
|
||||||
chapters.forEach {
|
|
||||||
it.status = Download.NOT_DOWNLOADED
|
|
||||||
it.download = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverses the sorting and requests an UI update.
|
|
||||||
*/
|
|
||||||
fun revertSortOrder() {
|
|
||||||
manga.setChapterOrder(if (sortDescending()) Manga.SORT_ASC else Manga.SORT_DESC)
|
|
||||||
db.updateFlags(manga).executeAsBlocking()
|
|
||||||
refreshChapters()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the read filter and requests an UI update.
|
|
||||||
* @param onlyUnread whether to display only unread chapters or all chapters.
|
|
||||||
*/
|
|
||||||
fun setUnreadFilter(onlyUnread: Boolean) {
|
|
||||||
manga.readFilter = if (onlyUnread) Manga.SHOW_UNREAD else Manga.SHOW_ALL
|
|
||||||
db.updateFlags(manga).executeAsBlocking()
|
|
||||||
refreshChapters()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the read filter and requests an UI update.
|
|
||||||
* @param onlyRead whether to display only read chapters or all chapters.
|
|
||||||
*/
|
|
||||||
fun setReadFilter(onlyRead: Boolean) {
|
|
||||||
manga.readFilter = if (onlyRead) Manga.SHOW_READ else Manga.SHOW_ALL
|
|
||||||
db.updateFlags(manga).executeAsBlocking()
|
|
||||||
refreshChapters()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the download filter and requests an UI update.
|
|
||||||
* @param onlyDownloaded whether to display only downloaded chapters or all chapters.
|
|
||||||
*/
|
|
||||||
fun setDownloadedFilter(onlyDownloaded: Boolean) {
|
|
||||||
manga.downloadedFilter = if (onlyDownloaded) Manga.SHOW_DOWNLOADED else Manga.SHOW_ALL
|
|
||||||
db.updateFlags(manga).executeAsBlocking()
|
|
||||||
refreshChapters()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the bookmark filter and requests an UI update.
|
|
||||||
* @param onlyBookmarked whether to display only bookmarked chapters or all chapters.
|
|
||||||
*/
|
|
||||||
fun setBookmarkedFilter(onlyBookmarked: Boolean) {
|
|
||||||
manga.bookmarkedFilter = if (onlyBookmarked) Manga.SHOW_BOOKMARKED else Manga.SHOW_ALL
|
|
||||||
db.updateFlags(manga).executeAsBlocking()
|
|
||||||
refreshChapters()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes all filters and requests an UI update.
|
|
||||||
*/
|
|
||||||
fun removeFilters() {
|
|
||||||
manga.readFilter = Manga.SHOW_ALL
|
|
||||||
manga.downloadedFilter = Manga.SHOW_ALL
|
|
||||||
manga.bookmarkedFilter = Manga.SHOW_ALL
|
|
||||||
db.updateFlags(manga).executeAsBlocking()
|
|
||||||
refreshChapters()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds manga to library
|
|
||||||
*/
|
|
||||||
fun addToLibrary() {
|
|
||||||
mangaFavoriteRelay.call(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the active display mode.
|
|
||||||
* @param mode the mode to set.
|
|
||||||
*/
|
|
||||||
fun setDisplayMode(mode: Int) {
|
|
||||||
manga.displayMode = mode
|
|
||||||
db.updateFlags(manga).executeAsBlocking()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the sorting method and requests an UI update.
|
|
||||||
* @param sort the sorting mode.
|
|
||||||
*/
|
|
||||||
fun setSorting(sort: Int) {
|
|
||||||
manga.sorting = sort
|
|
||||||
db.updateFlags(manga).executeAsBlocking()
|
|
||||||
refreshChapters()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the display only downloaded filter is enabled.
|
|
||||||
*/
|
|
||||||
fun onlyDownloaded(): Boolean {
|
|
||||||
return manga.downloadedFilter == Manga.SHOW_DOWNLOADED
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the display only downloaded filter is enabled.
|
|
||||||
*/
|
|
||||||
fun onlyBookmarked(): Boolean {
|
|
||||||
return manga.bookmarkedFilter == Manga.SHOW_BOOKMARKED
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the display only unread filter is enabled.
|
|
||||||
*/
|
|
||||||
fun onlyUnread(): Boolean {
|
|
||||||
return manga.readFilter == Manga.SHOW_UNREAD
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the display only read filter is enabled.
|
|
||||||
*/
|
|
||||||
fun onlyRead(): Boolean {
|
|
||||||
return manga.readFilter == Manga.SHOW_READ
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the sorting method is descending or ascending.
|
|
||||||
*/
|
|
||||||
fun sortDescending(): Boolean {
|
|
||||||
return manga.sortDescending()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getFirstUnreadIndex(): Int? {
|
|
||||||
if (!manga.favorite) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
val index = chapters.sortedByDescending { it.source_order }.indexOfFirst { !it.read }
|
|
||||||
return if (sortDescending()) (chapters.size - 1) - index
|
|
||||||
else index
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.manga.chapter
|
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.os.Bundle
|
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
|
||||||
import com.bluelinelabs.conductor.Controller
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
|
||||||
|
|
||||||
class DeleteChaptersDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
|
|
||||||
where T : Controller, T : DeleteChaptersDialog.Listener {
|
|
||||||
|
|
||||||
constructor(target: T) : this() {
|
|
||||||
targetController = target
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
|
||||||
return MaterialDialog(activity!!).show {
|
|
||||||
message(R.string.confirm_delete_chapters)
|
|
||||||
positiveButton(android.R.string.yes) {
|
|
||||||
(targetController as? Listener)?.deleteChapters()
|
|
||||||
}
|
|
||||||
negativeButton(android.R.string.no)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Listener {
|
|
||||||
fun deleteChapters()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,76 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.manga.chapter
|
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.os.Bundle
|
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
|
||||||
import com.afollestad.materialdialogs.customview.customView
|
|
||||||
import com.bluelinelabs.conductor.Controller
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
|
||||||
import eu.kanade.tachiyomi.widget.DialogCustomDownloadView
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dialog used to let user select amount of chapters to download.
|
|
||||||
*/
|
|
||||||
class DownloadCustomChaptersDialog<T> : DialogController
|
|
||||||
where T : Controller, T : DownloadCustomChaptersDialog.Listener {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Maximum number of chapters to download in download chooser.
|
|
||||||
*/
|
|
||||||
private val maxChapters: Int
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize dialog.
|
|
||||||
* @param maxChapters maximal number of chapters that user can download.
|
|
||||||
*/
|
|
||||||
constructor(target: T, maxChapters: Int) : super(Bundle().apply {
|
|
||||||
// Add maximum number of chapters to download value to bundle.
|
|
||||||
putInt(KEY_ITEM_MAX, maxChapters)
|
|
||||||
}) {
|
|
||||||
targetController = target
|
|
||||||
this.maxChapters = maxChapters
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restore dialog.
|
|
||||||
* @param bundle bundle containing data from state restore.
|
|
||||||
*/
|
|
||||||
@Suppress("unused")
|
|
||||||
constructor(bundle: Bundle) : super(bundle) {
|
|
||||||
// Get maximum chapters to download from bundle
|
|
||||||
val maxChapters = bundle.getInt(KEY_ITEM_MAX, 0)
|
|
||||||
this.maxChapters = maxChapters
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when dialog is being created.
|
|
||||||
*/
|
|
||||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
|
||||||
val activity = activity!!
|
|
||||||
|
|
||||||
// Initialize view that lets user select number of chapters to download.
|
|
||||||
val view = DialogCustomDownloadView(activity).apply {
|
|
||||||
setMinMax(0, maxChapters)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build dialog.
|
|
||||||
// when positive dialog is pressed call custom listener.
|
|
||||||
return MaterialDialog(activity)
|
|
||||||
.title(R.string.custom_download)
|
|
||||||
.customView(view = view, scrollable = true)
|
|
||||||
.positiveButton(android.R.string.ok) {
|
|
||||||
(targetController as? Listener)?.downloadCustomChapters(view.amount)
|
|
||||||
}
|
|
||||||
.negativeButton(android.R.string.cancel)
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Listener {
|
|
||||||
fun downloadCustomChapters(amount: Int)
|
|
||||||
}
|
|
||||||
|
|
||||||
private companion object {
|
|
||||||
// Key to retrieve max chapters from bundle on process death.
|
|
||||||
const val KEY_ITEM_MAX = "DownloadCustomChaptersDialog.int.maxChapters"
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,860 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.manga.info
|
|
||||||
|
|
||||||
import android.animation.Animator
|
|
||||||
import android.animation.AnimatorListenerAdapter
|
|
||||||
import android.animation.AnimatorSet
|
|
||||||
import android.animation.ObjectAnimator
|
|
||||||
import android.app.Activity
|
|
||||||
import android.app.PendingIntent
|
|
||||||
import android.content.ClipData
|
|
||||||
import android.content.ClipboardManager
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.res.Configuration
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.graphics.drawable.Drawable
|
|
||||||
import android.os.Build
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuInflater
|
|
||||||
import android.view.MenuItem
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.view.animation.DecelerateInterpolator
|
|
||||||
import android.widget.ImageView
|
|
||||||
import androidx.core.content.pm.ShortcutInfoCompat
|
|
||||||
import androidx.core.content.pm.ShortcutManagerCompat
|
|
||||||
import androidx.core.graphics.drawable.IconCompat
|
|
||||||
import androidx.transition.ChangeBounds
|
|
||||||
import androidx.transition.ChangeImageTransform
|
|
||||||
import androidx.transition.TransitionManager
|
|
||||||
import androidx.transition.TransitionSet
|
|
||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
|
||||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
|
||||||
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
|
|
||||||
import com.bumptech.glide.request.target.CustomTarget
|
|
||||||
import com.bumptech.glide.request.transition.Transition
|
|
||||||
import com.bumptech.glide.signature.ObjectKey
|
|
||||||
import com.google.android.material.snackbar.BaseTransientBottomBar
|
|
||||||
import com.google.android.material.snackbar.Snackbar
|
|
||||||
import com.jakewharton.rxbinding.support.v4.widget.refreshes
|
|
||||||
import com.jakewharton.rxbinding.view.clicks
|
|
||||||
import com.jakewharton.rxbinding.view.longClicks
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Category
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
|
||||||
import eu.kanade.tachiyomi.data.glide.GlideApp
|
|
||||||
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|
||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
|
||||||
import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchController
|
|
||||||
import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
|
|
||||||
import eu.kanade.tachiyomi.ui.library.LibraryController
|
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
|
||||||
import eu.kanade.tachiyomi.ui.manga.ChooseShapeDialog
|
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
|
||||||
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
|
|
||||||
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
|
|
||||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
|
||||||
import eu.kanade.tachiyomi.util.view.doOnApplyWindowInsets
|
|
||||||
import eu.kanade.tachiyomi.util.view.marginBottom
|
|
||||||
import eu.kanade.tachiyomi.util.view.snack
|
|
||||||
import eu.kanade.tachiyomi.util.view.updateLayoutParams
|
|
||||||
import eu.kanade.tachiyomi.util.view.updatePaddingRelative
|
|
||||||
import jp.wasabeef.glide.transformations.CropSquareTransformation
|
|
||||||
import jp.wasabeef.glide.transformations.MaskTransformation
|
|
||||||
import kotlinx.android.synthetic.main.manga_info_controller.*
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import java.io.File
|
|
||||||
import java.text.DateFormat
|
|
||||||
import java.text.DecimalFormat
|
|
||||||
import java.util.Date
|
|
||||||
import kotlin.math.max
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fragment that shows manga information.
|
|
||||||
* Uses R.layout.manga_info_controller.
|
|
||||||
* UI related actions should be called from here.
|
|
||||||
*/
|
|
||||||
class MangaInfoController : NucleusController<MangaInfoPresenter>(),
|
|
||||||
ChangeMangaCategoriesDialog.Listener {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Preferences helper.
|
|
||||||
*/
|
|
||||||
private val preferences: PreferencesHelper by injectLazy()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Snackbar containing an error message when a request fails.
|
|
||||||
*/
|
|
||||||
private var snack: Snackbar? = null
|
|
||||||
|
|
||||||
private var container:View? = null
|
|
||||||
|
|
||||||
// Hold a reference to the current animator,
|
|
||||||
// so that it can be canceled mid-way.
|
|
||||||
private var currentAnimator: Animator? = null
|
|
||||||
|
|
||||||
// The system "short" animation time duration, in milliseconds. This
|
|
||||||
// duration is ideal for subtle animations or animations that occur
|
|
||||||
// very frequently.
|
|
||||||
private var shortAnimationDuration: Int = 0
|
|
||||||
|
|
||||||
private var setUpFullCover = false
|
|
||||||
|
|
||||||
var fullRes:Drawable? = null
|
|
||||||
|
|
||||||
private val dateFormat: DateFormat by lazy {
|
|
||||||
preferences.dateFormat().getOrDefault()
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
setHasOptionsMenu(true)
|
|
||||||
setOptionsMenuHidden(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createPresenter(): MangaInfoPresenter {
|
|
||||||
val ctrl = parentController as MangaController
|
|
||||||
return MangaInfoPresenter(ctrl.manga!!, ctrl.source!!,
|
|
||||||
ctrl.chapterCountRelay, ctrl.lastUpdateRelay, ctrl.mangaFavoriteRelay)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
|
||||||
return inflater.inflate(R.layout.manga_info_controller, container, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View) {
|
|
||||||
super.onViewCreated(view)
|
|
||||||
setUpFullCover = false
|
|
||||||
// Set onclickListener to toggle favorite when FAB clicked.
|
|
||||||
fab_favorite.clicks().subscribeUntilDestroy { onFabClick() }
|
|
||||||
|
|
||||||
// Set onLongClickListener to manage categories when FAB is clicked.
|
|
||||||
fab_favorite.longClicks().subscribeUntilDestroy { onFabLongClick() }
|
|
||||||
|
|
||||||
// Set SwipeRefresh to refresh manga data.
|
|
||||||
swipe_refresh.refreshes().subscribeUntilDestroy { fetchMangaFromSource() }
|
|
||||||
|
|
||||||
manga_full_title.longClicks().subscribeUntilDestroy {
|
|
||||||
copyToClipboard(view.context.getString(R.string.title), manga_full_title.text
|
|
||||||
.toString(), R.string.manga_info_full_title_label)
|
|
||||||
}
|
|
||||||
|
|
||||||
manga_full_title.clicks().subscribeUntilDestroy {
|
|
||||||
performGlobalSearch(manga_full_title.text.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
manga_artist.longClicks().subscribeUntilDestroy {
|
|
||||||
copyToClipboard(manga_artist_label.text.toString(), manga_artist.text.toString(), R
|
|
||||||
.string.manga_info_artist_label)
|
|
||||||
}
|
|
||||||
|
|
||||||
manga_artist.clicks().subscribeUntilDestroy {
|
|
||||||
performGlobalSearch(manga_artist.text.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
manga_author.longClicks().subscribeUntilDestroy {
|
|
||||||
copyToClipboard(manga_author.text.toString(), manga_author.text.toString(), R.string
|
|
||||||
.manga_info_author_label)
|
|
||||||
}
|
|
||||||
|
|
||||||
manga_author.clicks().subscribeUntilDestroy {
|
|
||||||
performGlobalSearch(manga_author.text.toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
manga_summary.longClicks().subscribeUntilDestroy {
|
|
||||||
copyToClipboard(view.context.getString(R.string.description), manga_summary.text
|
|
||||||
.toString(), R.string.description)
|
|
||||||
}
|
|
||||||
|
|
||||||
manga_genres_tags.setOnTagClickListener { tag -> performLocalSearch(tag) }
|
|
||||||
|
|
||||||
manga_cover.clicks().subscribeUntilDestroy {
|
|
||||||
if (manga_cover.drawable != null) zoomImageFromThumb(manga_cover, manga_cover.drawable)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrieve and cache the system's default "short" animation time.
|
|
||||||
shortAnimationDuration = resources?.getInteger(android.R.integer.config_shortAnimTime) ?: 0
|
|
||||||
|
|
||||||
manga_cover.longClicks().subscribeUntilDestroy {
|
|
||||||
copyToClipboard(view.context.getString(R.string.title), presenter.manga.currentTitle(), R.string
|
|
||||||
.manga_info_full_title_label)
|
|
||||||
}
|
|
||||||
container = (view as ViewGroup).findViewById(R.id.manga_info_layout) as? View
|
|
||||||
val bottomM = manga_genres_tags.marginBottom
|
|
||||||
val fabBaseMarginBottom = fab_favorite.marginBottom
|
|
||||||
val mangaCoverMarginBottom = manga_cover.marginBottom
|
|
||||||
val fullMarginBottom = manga_cover_full?.marginBottom ?: 0
|
|
||||||
manga_cover.viewTreeObserver.addOnGlobalLayoutListener {
|
|
||||||
setFullCoverToThumb()
|
|
||||||
}
|
|
||||||
container?.setOnApplyWindowInsetsListener { _, insets ->
|
|
||||||
if (resources?.configuration?.orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
|
||||||
fab_favorite?.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
|
||||||
bottomMargin = fabBaseMarginBottom + insets.systemWindowInsetBottom
|
|
||||||
}
|
|
||||||
manga_cover?.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
|
||||||
bottomMargin = mangaCoverMarginBottom + insets.systemWindowInsetBottom
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
manga_genres_tags?.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
|
||||||
bottomMargin = bottomM + insets.systemWindowInsetBottom
|
|
||||||
}
|
|
||||||
}
|
|
||||||
manga_cover_full?.updateLayoutParams<ViewGroup.MarginLayoutParams> {
|
|
||||||
bottomMargin = fullMarginBottom + insets.systemWindowInsetBottom
|
|
||||||
}
|
|
||||||
insets
|
|
||||||
}
|
|
||||||
info_scrollview.doOnApplyWindowInsets { v, insets, padding ->
|
|
||||||
if (resources?.configuration?.orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
|
||||||
v.updatePaddingRelative(
|
|
||||||
bottom = max(padding.bottom, insets.systemWindowInsetBottom)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
|
||||||
inflater.inflate(R.menu.manga_info, menu)
|
|
||||||
|
|
||||||
val editItem = menu.findItem(R.id.action_edit)
|
|
||||||
editItem.isVisible = presenter.manga.favorite &&
|
|
||||||
!(parentController as MangaController).isLockedFromSearch
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
|
||||||
when (item.itemId) {
|
|
||||||
//R.id.action_edit -> EditMangaDialog(this, presenter.manga).showDialog(router)
|
|
||||||
R.id.action_open_in_web_view -> openInWebView()
|
|
||||||
R.id.action_share -> prepareToShareManga()
|
|
||||||
R.id.action_add_to_home_screen -> addToHomeScreen()
|
|
||||||
}
|
|
||||||
return super.onOptionsItemSelected(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if manga is initialized.
|
|
||||||
* If true update view with manga information,
|
|
||||||
* if false fetch manga information
|
|
||||||
*
|
|
||||||
* @param manga manga object containing information about manga.
|
|
||||||
* @param source the source of the manga.
|
|
||||||
*/
|
|
||||||
fun onNextManga(manga: Manga, source: Source) {
|
|
||||||
if (manga.initialized) {
|
|
||||||
// Update view.
|
|
||||||
setMangaInfo(manga, source)
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// Initialize manga.
|
|
||||||
fetchMangaFromSource()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the view with manga information.
|
|
||||||
*
|
|
||||||
* @param manga manga object containing information about manga.
|
|
||||||
* @param source the source of the manga.
|
|
||||||
*/
|
|
||||||
private fun setMangaInfo(manga: Manga, source: Source?) {
|
|
||||||
val view = view ?: return
|
|
||||||
|
|
||||||
//update full title TextView.
|
|
||||||
manga_full_title.text = if (manga.currentTitle().isBlank()) {
|
|
||||||
view.context.getString(R.string.unknown)
|
|
||||||
} else {
|
|
||||||
manga.currentTitle()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update artist TextView.
|
|
||||||
manga_artist.text = if (manga.currentArtist().isNullOrBlank()) {
|
|
||||||
view.context.getString(R.string.unknown)
|
|
||||||
} else {
|
|
||||||
manga.currentArtist()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update author TextView.
|
|
||||||
manga_author.text = if (manga.currentAuthor().isNullOrBlank()) {
|
|
||||||
view.context.getString(R.string.unknown)
|
|
||||||
} else {
|
|
||||||
manga.currentAuthor()
|
|
||||||
}
|
|
||||||
|
|
||||||
// If manga source is known update source TextView.
|
|
||||||
manga_source.text = source?.toString() ?: view.context.getString(R.string.unknown)
|
|
||||||
|
|
||||||
// Update genres list
|
|
||||||
if (manga.currentGenres().isNullOrBlank().not()) {
|
|
||||||
manga_genres_tags.setTags(manga.currentGenres()?.split(", "))
|
|
||||||
}
|
|
||||||
else manga_genres_tags.setTags(emptyList())
|
|
||||||
|
|
||||||
// Update description TextView.
|
|
||||||
manga_summary.text = if (manga.currentDesc().isNullOrBlank()) {
|
|
||||||
view.context.getString(R.string.unknown)
|
|
||||||
} else {
|
|
||||||
manga.currentDesc()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update status TextView.
|
|
||||||
manga_status.setText(when (manga.status) {
|
|
||||||
SManga.ONGOING -> R.string.ongoing
|
|
||||||
SManga.COMPLETED -> R.string.completed
|
|
||||||
SManga.LICENSED -> R.string.licensed
|
|
||||||
else -> R.string.unknown
|
|
||||||
})
|
|
||||||
|
|
||||||
// Set the favorite drawable to the correct one.
|
|
||||||
setFavoriteDrawable(manga.favorite)
|
|
||||||
activity?.invalidateOptionsMenu()
|
|
||||||
|
|
||||||
// Set cover if it wasn't already.
|
|
||||||
if (!manga.thumbnail_url.isNullOrEmpty()) {
|
|
||||||
GlideApp.with(view.context)
|
|
||||||
.load(manga)
|
|
||||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
|
||||||
.signature(ObjectKey(MangaImpl.getLastCoverFetch(manga.id!!).toString()))
|
|
||||||
.transition(DrawableTransitionOptions.withCrossFade())
|
|
||||||
//.centerCrop()
|
|
||||||
.into(manga_cover)
|
|
||||||
if (manga_cover_full != null) {
|
|
||||||
GlideApp.with(view.context).asDrawable().load(manga)
|
|
||||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
|
||||||
.signature(ObjectKey(MangaImpl.getLastCoverFetch(manga.id!!).toString()))
|
|
||||||
.override(CustomTarget.SIZE_ORIGINAL, CustomTarget.SIZE_ORIGINAL)
|
|
||||||
.into(object : CustomTarget<Drawable>() {
|
|
||||||
override fun onResourceReady(resource: Drawable,
|
|
||||||
transition: Transition<in Drawable>?
|
|
||||||
) {
|
|
||||||
fullRes = resource
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLoadCleared(placeholder: Drawable?) { }
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (backdrop != null) {
|
|
||||||
GlideApp.with(view.context)
|
|
||||||
.load(manga)
|
|
||||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
|
||||||
.signature(ObjectKey(MangaImpl.getLastCoverFetch(manga.id!!).toString()))
|
|
||||||
.transition(DrawableTransitionOptions.withCrossFade())
|
|
||||||
.centerCrop()
|
|
||||||
.into(backdrop)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActivityResumed(activity: Activity) {
|
|
||||||
super.onActivityResumed(activity)
|
|
||||||
setFavoriteDrawable(presenter.manga.favorite)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView(view: View) {
|
|
||||||
manga_genres_tags.setOnTagClickListener(null)
|
|
||||||
snack?.dismiss()
|
|
||||||
super.onDestroyView(view)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update chapter count TextView.
|
|
||||||
*
|
|
||||||
* @param count number of chapters.
|
|
||||||
*/
|
|
||||||
fun setChapterCount(count: Float) {
|
|
||||||
if (count > 0f) {
|
|
||||||
manga_chapters?.text = DecimalFormat("#.#").format(count)
|
|
||||||
} else {
|
|
||||||
manga_chapters?.text = resources?.getString(R.string.unknown)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setLastUpdateDate(date: Date) {
|
|
||||||
if (date.time != 0L) {
|
|
||||||
manga_status?.text = dateFormat.format(date)
|
|
||||||
} else {
|
|
||||||
manga_status?.text = resources?.getString(R.string.unknown)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Toggles the favorite status and asks for confirmation to delete downloaded chapters.
|
|
||||||
*/
|
|
||||||
private fun toggleFavorite() {
|
|
||||||
presenter.toggleFavorite()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun openInWebView() {
|
|
||||||
val source = presenter.source as? HttpSource ?: return
|
|
||||||
|
|
||||||
val url = try {
|
|
||||||
source.mangaDetailsRequest(presenter.manga).url.toString()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val activity = activity ?: return
|
|
||||||
val intent = WebViewActivity.newIntent(activity.applicationContext, source.id, url, presenter.manga
|
|
||||||
.originalTitle())
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called to run Intent with [Intent.ACTION_SEND], which show share dialog.
|
|
||||||
*/
|
|
||||||
private fun prepareToShareManga() {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && manga_cover.drawable != null)
|
|
||||||
GlideApp.with(activity!!).asBitmap().load(presenter.manga).into(object :
|
|
||||||
CustomTarget<Bitmap>() {
|
|
||||||
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
|
|
||||||
presenter.shareManga(resource)
|
|
||||||
}
|
|
||||||
override fun onLoadCleared(placeholder: Drawable?) {}
|
|
||||||
|
|
||||||
override fun onLoadFailed(errorDrawable: Drawable?) {
|
|
||||||
shareManga()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
else shareManga()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called to run Intent with [Intent.ACTION_SEND], which show share dialog.
|
|
||||||
*/
|
|
||||||
fun shareManga(cover: File? = null) {
|
|
||||||
val context = view?.context ?: return
|
|
||||||
|
|
||||||
val source = presenter.source as? HttpSource ?: return
|
|
||||||
val stream = cover?.getUriCompat(context)
|
|
||||||
try {
|
|
||||||
val url = source.mangaDetailsRequest(presenter.manga).url.toString()
|
|
||||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
|
||||||
type = "text/*"
|
|
||||||
putExtra(Intent.EXTRA_TEXT, url)
|
|
||||||
putExtra(Intent.EXTRA_TITLE, presenter.manga.currentTitle())
|
|
||||||
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
|
||||||
if (stream != null) {
|
|
||||||
clipData = ClipData.newRawUri(null, stream)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
startActivity(Intent.createChooser(intent, context.getString(R.string.action_share)))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
context.toast(e.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update FAB with correct drawable.
|
|
||||||
*
|
|
||||||
* @param isFavorite determines if manga is favorite or not.
|
|
||||||
*/
|
|
||||||
private fun setFavoriteDrawable(isFavorite: Boolean) {
|
|
||||||
// Set the Favorite drawable to the correct one.
|
|
||||||
// Border drawable if false, filled drawable if true.
|
|
||||||
fab_favorite?.setImageResource(
|
|
||||||
when {
|
|
||||||
(parentController as MangaController).isLockedFromSearch -> R.drawable.ic_lock_white_24dp
|
|
||||||
isFavorite -> R.drawable.ic_bookmark_white_24dp
|
|
||||||
else -> R.drawable.ic_add_to_library_24dp
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start fetching manga information from source.
|
|
||||||
*/
|
|
||||||
private fun fetchMangaFromSource() {
|
|
||||||
setRefreshing(true)
|
|
||||||
// Call presenter and start fetching manga information
|
|
||||||
presenter.fetchMangaFromSource()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update swipe refresh to stop showing refresh in progress spinner.
|
|
||||||
*/
|
|
||||||
fun onFetchMangaDone() {
|
|
||||||
setRefreshing(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update swipe refresh to start showing refresh in progress spinner.
|
|
||||||
*/
|
|
||||||
fun onFetchMangaError(error: Throwable) {
|
|
||||||
setRefreshing(false)
|
|
||||||
activity?.toast(error.message)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set swipe refresh status.
|
|
||||||
*
|
|
||||||
* @param value whether it should be refreshing or not.
|
|
||||||
*/
|
|
||||||
private fun setRefreshing(value: Boolean) {
|
|
||||||
swipe_refresh?.isRefreshing = value
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when the fab is clicked.
|
|
||||||
*/
|
|
||||||
private fun onFabClick() {
|
|
||||||
if ((parentController as MangaController).isLockedFromSearch) {
|
|
||||||
SecureActivityDelegate.promptLockIfNeeded(activity)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val manga = presenter.manga
|
|
||||||
toggleFavorite()
|
|
||||||
if (manga.favorite) {
|
|
||||||
val categories = presenter.getCategories()
|
|
||||||
val defaultCategoryId = preferences.defaultCategory()
|
|
||||||
val defaultCategory = categories.find { it.id == defaultCategoryId }
|
|
||||||
when {
|
|
||||||
defaultCategory != null -> presenter.moveMangaToCategory(manga, defaultCategory)
|
|
||||||
defaultCategoryId == 0 || categories.isEmpty() -> // 'Default' or no category
|
|
||||||
presenter.moveMangaToCategory(manga, null)
|
|
||||||
else -> {
|
|
||||||
val ids = presenter.getMangaCategoryIds(manga)
|
|
||||||
val preselected = ids.mapNotNull { id ->
|
|
||||||
categories.indexOfFirst { it.id == id }.takeIf { it != -1 }
|
|
||||||
}.toTypedArray()
|
|
||||||
|
|
||||||
ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected)
|
|
||||||
.showDialog(router)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
showAddedSnack()
|
|
||||||
} else {
|
|
||||||
showRemovedSnack()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showAddedSnack() {
|
|
||||||
val view = container
|
|
||||||
snack?.dismiss()
|
|
||||||
snack = view?.snack(view.context.getString(R.string.manga_added_library))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showRemovedSnack() {
|
|
||||||
val view = container
|
|
||||||
snack?.dismiss()
|
|
||||||
if (view != null) {
|
|
||||||
snack = view.snack(view.context.getString(R.string.manga_removed_library), Snackbar.LENGTH_INDEFINITE) {
|
|
||||||
setAction(R.string.action_undo) {
|
|
||||||
presenter.setFavorite(true)
|
|
||||||
}
|
|
||||||
addCallback(object : BaseTransientBottomBar.BaseCallback<Snackbar>() {
|
|
||||||
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
|
|
||||||
super.onDismissed(transientBottomBar, event)
|
|
||||||
if (!presenter.manga.favorite)
|
|
||||||
presenter.confirmDeletion()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
(activity as? MainActivity)?.setUndoSnackBar(snack, fab_favorite)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when the fab is long clicked.
|
|
||||||
*/
|
|
||||||
private fun onFabLongClick() {
|
|
||||||
val manga = presenter.manga
|
|
||||||
if (!manga.favorite) {
|
|
||||||
toggleFavorite()
|
|
||||||
showAddedSnack()
|
|
||||||
}
|
|
||||||
val categories = presenter.getCategories()
|
|
||||||
if (categories.isEmpty()) {
|
|
||||||
// no categories exist, display a message about adding categories
|
|
||||||
snack = container?.snack(R.string.action_add_category)
|
|
||||||
} else {
|
|
||||||
val ids = presenter.getMangaCategoryIds(manga)
|
|
||||||
val preselected = ids.mapNotNull { id ->
|
|
||||||
categories.indexOfFirst { it.id == id }.takeIf { it != -1 }
|
|
||||||
}.toTypedArray()
|
|
||||||
|
|
||||||
ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected)
|
|
||||||
.showDialog(router)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>) {
|
|
||||||
val manga = mangas.firstOrNull() ?: return
|
|
||||||
presenter.moveMangaToCategories(manga, categories)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a shortcut of the manga to the home screen
|
|
||||||
*/
|
|
||||||
private fun addToHomeScreen() {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
// TODO are transformations really unsupported or is it just the Pixel Launcher?
|
|
||||||
createShortcutForShape()
|
|
||||||
} else {
|
|
||||||
ChooseShapeDialog(this).showDialog(router)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves the bitmap of the shortcut with the requested shape and calls [createShortcut] when
|
|
||||||
* the resource is available.
|
|
||||||
*
|
|
||||||
* @param i The shape index to apply. Defaults to circle crop transformation.
|
|
||||||
*/
|
|
||||||
fun createShortcutForShape(i: Int = 0) {
|
|
||||||
if (activity == null) return
|
|
||||||
GlideApp.with(activity!!)
|
|
||||||
.asBitmap()
|
|
||||||
.load(presenter.manga)
|
|
||||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
|
||||||
.apply {
|
|
||||||
when (i) {
|
|
||||||
0 -> circleCrop()
|
|
||||||
1 -> transform(RoundedCorners(5))
|
|
||||||
2 -> transform(CropSquareTransformation())
|
|
||||||
3 -> centerCrop().transform(MaskTransformation(R.drawable.mask_star))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.into(object : CustomTarget<Bitmap>(96, 96) {
|
|
||||||
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
|
|
||||||
createShortcut(resource)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLoadCleared(placeholder: Drawable?) { }
|
|
||||||
|
|
||||||
override fun onLoadFailed(errorDrawable: Drawable?) {
|
|
||||||
activity?.toast(R.string.icon_creation_fail)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copies a string to clipboard
|
|
||||||
*
|
|
||||||
* @param label Label to show to the user describing the content
|
|
||||||
* @param content the actual text to copy to the board
|
|
||||||
*/
|
|
||||||
private fun copyToClipboard(label: String, content: String, resId: Int) {
|
|
||||||
if (content.isBlank()) return
|
|
||||||
|
|
||||||
val activity = activity ?: return
|
|
||||||
val view = view ?: return
|
|
||||||
|
|
||||||
val clipboard = activity.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
|
||||||
clipboard.setPrimaryClip(ClipData.newPlainText(label, content))
|
|
||||||
|
|
||||||
snack = container?.snack(view.context.getString(R.string.copied_to_clipboard, view.context
|
|
||||||
.getString(resId)))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Perform a global search using the provided query.
|
|
||||||
*
|
|
||||||
* @param query the search query to pass to the search controller
|
|
||||||
*/
|
|
||||||
private fun performGlobalSearch(query: String) {
|
|
||||||
if ((parentController as MangaController).isLockedFromSearch)
|
|
||||||
return
|
|
||||||
val router = parentController?.router ?: return
|
|
||||||
router.pushController(CatalogueSearchController(query).withFadeTransaction())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Perform a local search using the provided query.
|
|
||||||
*
|
|
||||||
* @param query the search query to pass to the library controller
|
|
||||||
*/
|
|
||||||
private fun performLocalSearch(query: String) {
|
|
||||||
val router = parentController?.router ?: return
|
|
||||||
val firstController = router.backstack.first()?.controller()
|
|
||||||
if (firstController is LibraryController && router.backstack.size == 2) {
|
|
||||||
router.handleBack()
|
|
||||||
firstController.search(query)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create shortcut using ShortcutManager.
|
|
||||||
*
|
|
||||||
* @param icon The image of the shortcut.
|
|
||||||
*/
|
|
||||||
private fun createShortcut(icon: Bitmap) {
|
|
||||||
val activity = activity ?: return
|
|
||||||
val mangaControllerArgs = parentController?.args ?: return
|
|
||||||
|
|
||||||
// Create the shortcut intent.
|
|
||||||
val shortcutIntent = activity.intent
|
|
||||||
.setAction(MainActivity.SHORTCUT_MANGA)
|
|
||||||
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
|
||||||
.putExtra(MangaController.MANGA_EXTRA,
|
|
||||||
mangaControllerArgs.getLong(MangaController.MANGA_EXTRA))
|
|
||||||
|
|
||||||
// Check if shortcut placement is supported
|
|
||||||
if (ShortcutManagerCompat.isRequestPinShortcutSupported(activity)) {
|
|
||||||
val shortcutId = "manga-shortcut-${presenter.manga.originalTitle()}-${presenter.source.name}"
|
|
||||||
|
|
||||||
// Create shortcut info
|
|
||||||
val shortcutInfo = ShortcutInfoCompat.Builder(activity, shortcutId)
|
|
||||||
.setShortLabel(presenter.manga.currentTitle())
|
|
||||||
.setIcon(IconCompat.createWithBitmap(icon))
|
|
||||||
.setIntent(shortcutIntent)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val successCallback = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
// Create the CallbackIntent.
|
|
||||||
val intent = ShortcutManagerCompat.createShortcutResultIntent(activity, shortcutInfo)
|
|
||||||
|
|
||||||
// Configure the intent so that the broadcast receiver gets the callback successfully.
|
|
||||||
PendingIntent.getBroadcast(activity, 0, intent, 0)
|
|
||||||
} else {
|
|
||||||
NotificationReceiver.shortcutCreatedBroadcast(activity)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Request shortcut.
|
|
||||||
ShortcutManagerCompat.requestPinShortcut(activity, shortcutInfo,
|
|
||||||
successCallback.intentSender)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateTitle() {
|
|
||||||
setMangaInfo(presenter.manga, presenter.source)
|
|
||||||
(parentController as? MangaController)?.updateTitle(presenter.manga)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setFullCoverToThumb() {
|
|
||||||
if (setUpFullCover) return
|
|
||||||
val expandedImageView = manga_cover_full ?: return
|
|
||||||
val thumbView = manga_cover
|
|
||||||
expandedImageView.pivotX = 0f
|
|
||||||
expandedImageView.pivotY = 0f
|
|
||||||
|
|
||||||
val layoutParams = expandedImageView.layoutParams
|
|
||||||
layoutParams.height = thumbView.height
|
|
||||||
layoutParams.width = thumbView.width
|
|
||||||
expandedImageView.layoutParams = layoutParams
|
|
||||||
expandedImageView.scaleType = ImageView.ScaleType.FIT_CENTER
|
|
||||||
setUpFullCover = expandedImageView.height > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun handleBack(): Boolean {
|
|
||||||
if (manga_cover_full?.visibility == View.VISIBLE &&
|
|
||||||
(parentController as? MangaController)?.tabLayout()?.selectedTabPosition == 0)
|
|
||||||
{
|
|
||||||
manga_cover_full?.performClick()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return super.handleBack()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun zoomImageFromThumb(thumbView: ImageView, cover: Drawable) {
|
|
||||||
// If there's an animation in progress, cancel it immediately and proceed with this one.
|
|
||||||
currentAnimator?.cancel()
|
|
||||||
|
|
||||||
// Load the high-resolution "zoomed-in" image.
|
|
||||||
val expandedImageView = manga_cover_full ?: return
|
|
||||||
val fullBackdrop = full_backdrop
|
|
||||||
val image = fullRes ?: return
|
|
||||||
expandedImageView.setImageDrawable(image)
|
|
||||||
|
|
||||||
// Hide the thumbnail and show the zoomed-in view. When the animation
|
|
||||||
// begins, it will position the zoomed-in view in the place of the
|
|
||||||
// thumbnail.
|
|
||||||
thumbView.alpha = 0f
|
|
||||||
expandedImageView.visibility = View.VISIBLE
|
|
||||||
fullBackdrop.visibility = View.VISIBLE
|
|
||||||
|
|
||||||
// Set the pivot point to 0 to match thumbnail
|
|
||||||
|
|
||||||
swipe_refresh.isEnabled = false
|
|
||||||
|
|
||||||
val layoutParams = expandedImageView.layoutParams
|
|
||||||
layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
|
|
||||||
layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT
|
|
||||||
expandedImageView.layoutParams = layoutParams
|
|
||||||
|
|
||||||
// TransitionSet for the full cover because using animation for this SUCKS
|
|
||||||
val transitionSet = TransitionSet()
|
|
||||||
val bound = ChangeBounds()
|
|
||||||
transitionSet.addTransition(bound)
|
|
||||||
val changeImageTransform = ChangeImageTransform()
|
|
||||||
transitionSet.addTransition(changeImageTransform)
|
|
||||||
transitionSet.duration = shortAnimationDuration.toLong()
|
|
||||||
TransitionManager.beginDelayedTransition(manga_info_layout, transitionSet)
|
|
||||||
|
|
||||||
// AnimationSet for backdrop because idk how to use TransitionSet
|
|
||||||
currentAnimator = AnimatorSet().apply {
|
|
||||||
play(
|
|
||||||
ObjectAnimator.ofFloat(fullBackdrop, View.ALPHA, 0f, 0.5f)
|
|
||||||
)
|
|
||||||
duration = shortAnimationDuration.toLong()
|
|
||||||
interpolator = DecelerateInterpolator()
|
|
||||||
addListener(object : AnimatorListenerAdapter() {
|
|
||||||
|
|
||||||
override fun onAnimationEnd(animation: Animator) {
|
|
||||||
TransitionManager.endTransitions(manga_info_layout)
|
|
||||||
currentAnimator = null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAnimationCancel(animation: Animator) {
|
|
||||||
TransitionManager.endTransitions(manga_info_layout)
|
|
||||||
currentAnimator = null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
start()
|
|
||||||
}
|
|
||||||
|
|
||||||
expandedImageView.setOnClickListener {
|
|
||||||
currentAnimator?.cancel()
|
|
||||||
|
|
||||||
val layoutParams = expandedImageView.layoutParams
|
|
||||||
layoutParams.height = thumbView.height
|
|
||||||
layoutParams.width = thumbView.width
|
|
||||||
expandedImageView.layoutParams = layoutParams
|
|
||||||
|
|
||||||
// Zoom out back to tc thumbnail
|
|
||||||
val transitionSet = TransitionSet()
|
|
||||||
val bound = ChangeBounds()
|
|
||||||
transitionSet.addTransition(bound)
|
|
||||||
val changeImageTransform = ChangeImageTransform()
|
|
||||||
transitionSet.addTransition(changeImageTransform)
|
|
||||||
transitionSet.duration = shortAnimationDuration.toLong()
|
|
||||||
TransitionManager.beginDelayedTransition(manga_info_layout, transitionSet)
|
|
||||||
|
|
||||||
// Animation to remove backdrop and hide the full cover
|
|
||||||
currentAnimator = AnimatorSet().apply {
|
|
||||||
play(ObjectAnimator.ofFloat(fullBackdrop, View.ALPHA, 0f))
|
|
||||||
duration = shortAnimationDuration.toLong()
|
|
||||||
interpolator = DecelerateInterpolator()
|
|
||||||
addListener(object : AnimatorListenerAdapter() {
|
|
||||||
|
|
||||||
override fun onAnimationEnd(animation: Animator) {
|
|
||||||
thumbView.alpha = 1f
|
|
||||||
expandedImageView.visibility = View.GONE
|
|
||||||
fullBackdrop.visibility = View.GONE
|
|
||||||
swipe_refresh.isEnabled = true
|
|
||||||
currentAnimator = null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAnimationCancel(animation: Animator) {
|
|
||||||
thumbView.alpha = 1f
|
|
||||||
expandedImageView.visibility = View.GONE
|
|
||||||
fullBackdrop.visibility = View.GONE
|
|
||||||
swipe_refresh.isEnabled = true
|
|
||||||
currentAnimator = null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,290 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.manga.info
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
|
||||||
import com.jakewharton.rxrelay.BehaviorRelay
|
|
||||||
import com.jakewharton.rxrelay.PublishRelay
|
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Category
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|
||||||
import eu.kanade.tachiyomi.source.LocalSource
|
|
||||||
import eu.kanade.tachiyomi.source.Source
|
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
|
||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
|
||||||
import eu.kanade.tachiyomi.util.lang.isNullOrUnsubscribed
|
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
|
||||||
import rx.Observable
|
|
||||||
import rx.Subscription
|
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
|
||||||
import rx.schedulers.Schedulers
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.io.OutputStream
|
|
||||||
import java.util.Date
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Presenter of MangaInfoFragment.
|
|
||||||
* Contains information and data for fragment.
|
|
||||||
* Observable updates should be called from here.
|
|
||||||
*/
|
|
||||||
class MangaInfoPresenter(
|
|
||||||
val manga: Manga,
|
|
||||||
val source: Source,
|
|
||||||
private val chapterCountRelay: BehaviorRelay<Float>,
|
|
||||||
private val lastUpdateRelay: BehaviorRelay<Date>,
|
|
||||||
private val mangaFavoriteRelay: PublishRelay<Boolean>,
|
|
||||||
private val db: DatabaseHelper = Injekt.get(),
|
|
||||||
private val downloadManager: DownloadManager = Injekt.get(),
|
|
||||||
private val coverCache: CoverCache = Injekt.get()
|
|
||||||
) : BasePresenter<MangaInfoController>() {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Subscription to send the manga to the view.
|
|
||||||
*/
|
|
||||||
private var viewMangaSubscription: Subscription? = null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Subscription to update the manga from the source.
|
|
||||||
*/
|
|
||||||
private var fetchMangaSubscription: Subscription? = null
|
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
|
||||||
super.onCreate(savedState)
|
|
||||||
sendMangaToView()
|
|
||||||
|
|
||||||
// Update chapter count
|
|
||||||
chapterCountRelay.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribeLatestCache(MangaInfoController::setChapterCount)
|
|
||||||
|
|
||||||
// Update favorite status
|
|
||||||
mangaFavoriteRelay.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe { setFavorite(it) }
|
|
||||||
.apply { add(this) }
|
|
||||||
|
|
||||||
//update last update date
|
|
||||||
lastUpdateRelay.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribeLatestCache(MangaInfoController::setLastUpdateDate)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends the active manga to the view.
|
|
||||||
*/
|
|
||||||
fun sendMangaToView() {
|
|
||||||
viewMangaSubscription?.let { remove(it) }
|
|
||||||
viewMangaSubscription = Observable.just(manga)
|
|
||||||
.subscribeLatestCache({ view, manga -> view.onNextManga(manga, source) })
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch manga information from source.
|
|
||||||
*/
|
|
||||||
fun fetchMangaFromSource() {
|
|
||||||
if (!fetchMangaSubscription.isNullOrUnsubscribed()) return
|
|
||||||
fetchMangaSubscription = Observable.defer { source.fetchMangaDetails(manga) }
|
|
||||||
.map { networkManga ->
|
|
||||||
manga.copyFrom(networkManga)
|
|
||||||
manga.initialized = true
|
|
||||||
db.insertManga(manga).executeAsBlocking()
|
|
||||||
MangaImpl.setLastCoverFetch(manga.id!!, Date().time)
|
|
||||||
manga
|
|
||||||
}
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.doOnNext { sendMangaToView() }
|
|
||||||
.subscribeFirst({ view, _ ->
|
|
||||||
view.onFetchMangaDone()
|
|
||||||
}, MangaInfoController::onFetchMangaError)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update favorite status of manga, (removes / adds) manga (to / from) library.
|
|
||||||
*
|
|
||||||
* @return the new status of the manga.
|
|
||||||
*/
|
|
||||||
fun toggleFavorite(): Boolean {
|
|
||||||
manga.favorite = !manga.favorite
|
|
||||||
db.insertManga(manga).executeAsBlocking()
|
|
||||||
sendMangaToView()
|
|
||||||
return manga.favorite
|
|
||||||
}
|
|
||||||
|
|
||||||
fun confirmDeletion() {
|
|
||||||
coverCache.deleteFromCache(manga.thumbnail_url)
|
|
||||||
db.resetMangaInfo(manga).executeAsBlocking()
|
|
||||||
downloadManager.deleteManga(manga, source)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setFavorite(favorite: Boolean) {
|
|
||||||
if (manga.favorite == favorite) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
toggleFavorite()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun shareManga(cover: Bitmap) {
|
|
||||||
val context = Injekt.get<Application>()
|
|
||||||
|
|
||||||
val destDir = File(context.cacheDir, "shared_image")
|
|
||||||
|
|
||||||
Observable.fromCallable { destDir.deleteRecursively() } // Keep only the last shared file
|
|
||||||
.map { saveImage(cover, destDir, manga) }
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribeFirst(
|
|
||||||
{ view, file -> view.shareManga(file) },
|
|
||||||
{ view, error -> view.shareManga() }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun saveImage(cover:Bitmap, directory: File, manga: Manga): File? {
|
|
||||||
directory.mkdirs()
|
|
||||||
|
|
||||||
// Build destination file.
|
|
||||||
val filename = DiskUtil.buildValidFilename("${manga.originalTitle()} - Cover.jpg")
|
|
||||||
|
|
||||||
val destFile = File(directory, filename)
|
|
||||||
val stream: OutputStream = FileOutputStream(destFile)
|
|
||||||
cover.compress(Bitmap.CompressFormat.JPEG, 75, stream)
|
|
||||||
stream.flush()
|
|
||||||
stream.close()
|
|
||||||
return destFile
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get user categories.
|
|
||||||
*
|
|
||||||
* @return List of categories, not including the default category
|
|
||||||
*/
|
|
||||||
fun getCategories(): List<Category> {
|
|
||||||
return db.getCategories().executeAsBlocking()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the category id's the manga is in, if the manga is not in a category, returns the default id.
|
|
||||||
*
|
|
||||||
* @param manga the manga to get categories from.
|
|
||||||
* @return Array of category ids the manga is in, if none returns default id
|
|
||||||
*/
|
|
||||||
fun getMangaCategoryIds(manga: Manga): Array<Int> {
|
|
||||||
val categories = db.getCategoriesForManga(manga).executeAsBlocking()
|
|
||||||
return categories.mapNotNull { it.id }.toTypedArray()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Move the given manga to categories.
|
|
||||||
*
|
|
||||||
* @param manga the manga to move.
|
|
||||||
* @param categories the selected categories.
|
|
||||||
*/
|
|
||||||
fun moveMangaToCategories(manga: Manga, categories: List<Category>) {
|
|
||||||
val mc = categories.filter { it.id != 0 }.map { MangaCategory.create(manga, it) }
|
|
||||||
db.setMangaCategories(mc, listOf(manga))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Move the given manga to the category.
|
|
||||||
*
|
|
||||||
* @param manga the manga to move.
|
|
||||||
* @param category the selected category, or null for default category.
|
|
||||||
*/
|
|
||||||
fun moveMangaToCategory(manga: Manga, category: Category?) {
|
|
||||||
moveMangaToCategories(manga, listOfNotNull(category))
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateManga(title:String?, author:String?, artist: String?, uri: Uri?,
|
|
||||||
description: String?, tags: Array<String>?) {
|
|
||||||
if (manga.source == LocalSource.ID) {
|
|
||||||
manga.title = if (title.isNullOrBlank()) manga.url else title.trim()
|
|
||||||
manga.author = author?.trim()
|
|
||||||
manga.artist = artist?.trim()
|
|
||||||
manga.description = description?.trim()
|
|
||||||
val tagsString = tags?.joinToString(", ") { it.capitalize() }
|
|
||||||
manga.genre = if (tags.isNullOrEmpty()) null else tagsString?.trim()
|
|
||||||
LocalSource(downloadManager.context).updateMangaInfo(manga)
|
|
||||||
db.updateMangaInfo(manga).executeAsBlocking()
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
var changed = false
|
|
||||||
val title = title?.trim()
|
|
||||||
if (!title.isNullOrBlank() && manga.originalTitle().isBlank()) {
|
|
||||||
manga.title = title
|
|
||||||
changed = true
|
|
||||||
}
|
|
||||||
else if (title.isNullOrBlank() && manga.currentTitle() != manga.originalTitle()) {
|
|
||||||
manga.title = manga.originalTitle()
|
|
||||||
changed = true
|
|
||||||
} else if (!title.isNullOrBlank() && title != manga.currentTitle()) {
|
|
||||||
manga.title = "${title}${SManga.splitter}${manga.originalTitle()}"
|
|
||||||
changed = true
|
|
||||||
}
|
|
||||||
|
|
||||||
val author = author?.trim()
|
|
||||||
if (author.isNullOrBlank() && manga.currentAuthor() != manga.originalAuthor()) {
|
|
||||||
manga.author = manga.originalAuthor()
|
|
||||||
changed = true
|
|
||||||
} else if (!author.isNullOrBlank() && author != manga.currentAuthor()) {
|
|
||||||
manga.author = "${author}${SManga.splitter}${manga.originalAuthor() ?: ""}"
|
|
||||||
changed = true
|
|
||||||
}
|
|
||||||
|
|
||||||
val artist = artist?.trim()
|
|
||||||
if (artist.isNullOrBlank() && manga.currentArtist() != manga.originalArtist()) {
|
|
||||||
manga.artist = manga.originalArtist()
|
|
||||||
changed = true
|
|
||||||
} else if (!artist.isNullOrBlank() && artist != manga.currentArtist()) {
|
|
||||||
manga.artist = "${artist}${SManga.splitter}${manga.originalArtist() ?: ""}"
|
|
||||||
changed = true
|
|
||||||
}
|
|
||||||
|
|
||||||
val description = description?.trim()
|
|
||||||
if (description.isNullOrBlank() && manga.currentDesc() != manga.originalDesc()) {
|
|
||||||
manga.description = manga.originalDesc()
|
|
||||||
changed = true
|
|
||||||
} else if (!description.isNullOrBlank() && description != manga.currentDesc()) {
|
|
||||||
manga.description = "${description}${SManga.splitter}${manga.originalDesc() ?: ""}"
|
|
||||||
changed = true
|
|
||||||
}
|
|
||||||
|
|
||||||
var tagsString = tags?.joinToString(", ")
|
|
||||||
if ((tagsString.isNullOrBlank() && manga.currentGenres() != manga.originalGenres())
|
|
||||||
|| tagsString == manga.originalGenres()) {
|
|
||||||
manga.genre = manga.originalGenres()
|
|
||||||
changed = true
|
|
||||||
} else if (!tagsString.isNullOrBlank() && tagsString != manga.currentGenres()) {
|
|
||||||
tagsString = tags?.joinToString(", ") { it.capitalize() }
|
|
||||||
manga.genre = "${tagsString}${SManga.splitter}${manga.originalGenres() ?: ""}"
|
|
||||||
changed = true
|
|
||||||
}
|
|
||||||
if (changed) db.updateMangaInfo(manga).executeAsBlocking()
|
|
||||||
}
|
|
||||||
if (uri != null) editCoverWithStream(uri)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun editCoverWithStream(uri: Uri): Boolean {
|
|
||||||
val inputStream = downloadManager.context.contentResolver.openInputStream(uri) ?:
|
|
||||||
return false
|
|
||||||
if (manga.source == LocalSource.ID) {
|
|
||||||
LocalSource.updateCover(downloadManager.context, manga, inputStream)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (manga.thumbnail_url != null && manga.favorite) {
|
|
||||||
Injekt.get<PreferencesHelper>().refreshCoversToo().set(false)
|
|
||||||
coverCache.copyToCache(manga.thumbnail_url!!, inputStream)
|
|
||||||
MangaImpl.setLastCoverFetch(manga.id!!, Date().time)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -6,7 +6,6 @@ import android.widget.NumberPicker
|
|||||||
import com.afollestad.materialdialogs.MaterialDialog
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
import com.afollestad.materialdialogs.customview.customView
|
import com.afollestad.materialdialogs.customview.customView
|
||||||
import com.afollestad.materialdialogs.customview.getCustomView
|
import com.afollestad.materialdialogs.customview.getCustomView
|
||||||
import com.bluelinelabs.conductor.Controller
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
@ -15,14 +14,15 @@ import uy.kohesive.injekt.Injekt
|
|||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
class SetTrackChaptersDialog<T> : DialogController
|
class SetTrackChaptersDialog<T> : DialogController
|
||||||
where T : Controller, T : SetTrackChaptersDialog.Listener {
|
where T : SetTrackChaptersDialog.Listener {
|
||||||
|
|
||||||
private val item: TrackItem
|
private val item: TrackItem
|
||||||
|
private lateinit var listener: Listener
|
||||||
|
|
||||||
constructor(target: T, item: TrackItem) : super(Bundle().apply {
|
constructor(target: T, item: TrackItem) : super(Bundle().apply {
|
||||||
putSerializable(KEY_ITEM_TRACK, item.track)
|
putSerializable(KEY_ITEM_TRACK, item.track)
|
||||||
}) {
|
}) {
|
||||||
targetController = target
|
listener = target
|
||||||
this.item = item
|
this.item = item
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,7 +45,7 @@ class SetTrackChaptersDialog<T> : DialogController
|
|||||||
// Remove focus to update selected number
|
// Remove focus to update selected number
|
||||||
val np: NumberPicker = view.findViewById(R.id.chapters_picker)
|
val np: NumberPicker = view.findViewById(R.id.chapters_picker)
|
||||||
np.clearFocus()
|
np.clearFocus()
|
||||||
(targetController as? Listener)?.setChaptersRead(item, np.value)
|
listener.setChaptersRead(item, np.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
val view = dialog.getCustomView()
|
val view = dialog.getCustomView()
|
||||||
|
@ -15,14 +15,15 @@ import uy.kohesive.injekt.Injekt
|
|||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
class SetTrackScoreDialog<T> : DialogController
|
class SetTrackScoreDialog<T> : DialogController
|
||||||
where T : Controller, T : SetTrackScoreDialog.Listener {
|
where T : SetTrackScoreDialog.Listener {
|
||||||
|
|
||||||
private val item: TrackItem
|
private val item: TrackItem
|
||||||
|
private lateinit var listener: Listener
|
||||||
|
|
||||||
constructor(target: T, item: TrackItem) : super(Bundle().apply {
|
constructor(target: T, item: TrackItem) : super(Bundle().apply {
|
||||||
putSerializable(KEY_ITEM_TRACK, item.track)
|
putSerializable(KEY_ITEM_TRACK, item.track)
|
||||||
}) {
|
}) {
|
||||||
targetController = target
|
listener = target
|
||||||
this.item = item
|
this.item = item
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -46,8 +47,7 @@ class SetTrackScoreDialog<T> : DialogController
|
|||||||
val np: NumberPicker = view.findViewById(R.id.score_picker)
|
val np: NumberPicker = view.findViewById(R.id.score_picker)
|
||||||
np.clearFocus()
|
np.clearFocus()
|
||||||
|
|
||||||
(targetController as? Listener)?.setScore(item, np.value)
|
listener.setScore(item, np.value)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -4,7 +4,6 @@ import android.app.Dialog
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
import com.afollestad.materialdialogs.list.listItemsSingleChoice
|
import com.afollestad.materialdialogs.list.listItemsSingleChoice
|
||||||
import com.bluelinelabs.conductor.Controller
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
import eu.kanade.tachiyomi.data.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
@ -13,14 +12,16 @@ import uy.kohesive.injekt.Injekt
|
|||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
class SetTrackStatusDialog<T> : DialogController
|
class SetTrackStatusDialog<T> : DialogController
|
||||||
where T : Controller, T : SetTrackStatusDialog.Listener {
|
where T : SetTrackStatusDialog.Listener {
|
||||||
|
|
||||||
private val item: TrackItem
|
private val item: TrackItem
|
||||||
|
private lateinit var listener: Listener
|
||||||
|
|
||||||
constructor(target: T, item: TrackItem) : super(Bundle().apply {
|
constructor(target: T, item: TrackItem) : super(Bundle().apply {
|
||||||
putSerializable(KEY_ITEM_TRACK, item.track)
|
putSerializable(KEY_ITEM_TRACK, item.track)
|
||||||
}) {
|
}) {
|
||||||
targetController = target
|
listener = target
|
||||||
|
// targetController = target
|
||||||
this.item = item
|
this.item = item
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,7 +44,7 @@ class SetTrackStatusDialog<T> : DialogController
|
|||||||
.listItemsSingleChoice(items = statusString, initialSelection = selectedIndex,
|
.listItemsSingleChoice(items = statusString, initialSelection = selectedIndex,
|
||||||
waitForPositiveButton = false)
|
waitForPositiveButton = false)
|
||||||
{ dialog, position, _ ->
|
{ dialog, position, _ ->
|
||||||
(targetController as? Listener)?.setStatus(item, position)
|
listener.setStatus(item, position)
|
||||||
dialog.dismiss()
|
dialog.dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
package eu.kanade.tachiyomi.ui.manga.track
|
package eu.kanade.tachiyomi.ui.manga.track
|
||||||
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import eu.kanade.tachiyomi.util.view.inflate
|
import eu.kanade.tachiyomi.util.view.inflate
|
||||||
|
|
||||||
class TrackAdapter(controller: TrackController) : RecyclerView.Adapter<TrackHolder>() {
|
class TrackAdapter(controller: OnClickListener) : RecyclerView.Adapter<TrackHolder>() {
|
||||||
|
|
||||||
var items = emptyList<TrackItem>()
|
var items = emptyList<TrackItem>()
|
||||||
set(value) {
|
set(value) {
|
||||||
@ -34,9 +35,13 @@ class TrackAdapter(controller: TrackController) : RecyclerView.Adapter<TrackHold
|
|||||||
holder.bind(items[position])
|
holder.bind(items[position])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun indexOf(item: TrackService?):Int {
|
||||||
|
return items.indexOfFirst { item?.id == it.service.id }
|
||||||
|
}
|
||||||
|
|
||||||
interface OnClickListener {
|
interface OnClickListener {
|
||||||
fun onLogoClick(position: Int)
|
fun onLogoClick(position: Int)
|
||||||
fun onTitleClick(position: Int)
|
fun onSetClick(position: Int)
|
||||||
fun onStatusClick(position: Int)
|
fun onStatusClick(position: Int)
|
||||||
fun onChaptersClick(position: Int)
|
fun onChaptersClick(position: Int)
|
||||||
fun onScoreClick(position: Int)
|
fun onScoreClick(position: Int)
|
||||||
|
@ -1,168 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.manga.track
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import com.jakewharton.rxbinding.support.v4.widget.refreshes
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
|
||||||
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
|
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
|
||||||
import eu.kanade.tachiyomi.util.view.RecyclerWindowInsetsListener
|
|
||||||
import eu.kanade.tachiyomi.util.view.gone
|
|
||||||
import eu.kanade.tachiyomi.util.view.invisible
|
|
||||||
import eu.kanade.tachiyomi.util.view.visible
|
|
||||||
import kotlinx.android.synthetic.main.track_controller.*
|
|
||||||
import timber.log.Timber
|
|
||||||
|
|
||||||
class TrackController : NucleusController<TrackPresenter>(),
|
|
||||||
TrackAdapter.OnClickListener,
|
|
||||||
SetTrackStatusDialog.Listener,
|
|
||||||
SetTrackChaptersDialog.Listener,
|
|
||||||
SetTrackScoreDialog.Listener {
|
|
||||||
|
|
||||||
private var adapter: TrackAdapter? = null
|
|
||||||
|
|
||||||
init {
|
|
||||||
// There's no menu, but this avoids a bug when coming from the catalogue, where the menu
|
|
||||||
// disappears if the searchview is expanded
|
|
||||||
setHasOptionsMenu(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun createPresenter(): TrackPresenter {
|
|
||||||
return TrackPresenter((parentController as MangaController).manga!!)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
|
||||||
return inflater.inflate(R.layout.track_controller, container, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View) {
|
|
||||||
super.onViewCreated(view)
|
|
||||||
|
|
||||||
if ((parentController as MangaController).isLockedFromSearch) {
|
|
||||||
swipe_refresh.invisible()
|
|
||||||
unlock_button.visible()
|
|
||||||
unlock_button.setOnClickListener {
|
|
||||||
SecureActivityDelegate.promptLockIfNeeded(activity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
adapter = TrackAdapter(this)
|
|
||||||
track_recycler.layoutManager = LinearLayoutManager(view.context)
|
|
||||||
track_recycler.adapter = adapter
|
|
||||||
track_recycler.setOnApplyWindowInsetsListener(RecyclerWindowInsetsListener)
|
|
||||||
swipe_refresh.isEnabled = false
|
|
||||||
swipe_refresh.refreshes().subscribeUntilDestroy { presenter.refresh() }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showTracking() {
|
|
||||||
swipe_refresh.visible()
|
|
||||||
unlock_button.gone()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActivityResumed(activity: Activity) {
|
|
||||||
super.onActivityResumed(activity)
|
|
||||||
if (!(parentController as MangaController).isLockedFromSearch) {
|
|
||||||
showTracking()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView(view: View) {
|
|
||||||
adapter = null
|
|
||||||
super.onDestroyView(view)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onNextTrackings(trackings: List<TrackItem>) {
|
|
||||||
val atLeastOneLink = trackings.any { it.track != null }
|
|
||||||
adapter?.items = trackings
|
|
||||||
swipe_refresh?.isEnabled = atLeastOneLink
|
|
||||||
(parentController as? MangaController)?.setTrackingIcon(atLeastOneLink)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onSearchResults(results: List<TrackSearch>) {
|
|
||||||
getSearchDialog()?.onSearchResults(results)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("UNUSED_PARAMETER")
|
|
||||||
fun onSearchResultsError(error: Throwable) {
|
|
||||||
Timber.e(error)
|
|
||||||
getSearchDialog()?.onSearchResultsError()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getSearchDialog(): TrackSearchDialog? {
|
|
||||||
return router.getControllerWithTag(TAG_SEARCH_CONTROLLER) as? TrackSearchDialog
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onRefreshDone() {
|
|
||||||
swipe_refresh?.isRefreshing = false
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onRefreshError(error: Throwable) {
|
|
||||||
swipe_refresh?.isRefreshing = false
|
|
||||||
activity?.toast(error.message)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLogoClick(position: Int) {
|
|
||||||
val track = adapter?.getItem(position)?.track ?: return
|
|
||||||
|
|
||||||
if (track.tracking_url.isNullOrBlank()) {
|
|
||||||
activity?.toast(R.string.url_not_set)
|
|
||||||
} else {
|
|
||||||
activity?.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(track.tracking_url)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTitleClick(position: Int) {
|
|
||||||
val item = adapter?.getItem(position) ?: return
|
|
||||||
TrackSearchDialog(this, item.service, item.track != null).showDialog(router,
|
|
||||||
TAG_SEARCH_CONTROLLER)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStatusClick(position: Int) {
|
|
||||||
val item = adapter?.getItem(position) ?: return
|
|
||||||
if (item.track == null) return
|
|
||||||
|
|
||||||
SetTrackStatusDialog(this, item).showDialog(router)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onChaptersClick(position: Int) {
|
|
||||||
val item = adapter?.getItem(position) ?: return
|
|
||||||
if (item.track == null) return
|
|
||||||
|
|
||||||
SetTrackChaptersDialog(this, item).showDialog(router)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onScoreClick(position: Int) {
|
|
||||||
val item = adapter?.getItem(position) ?: return
|
|
||||||
if (item.track == null) return
|
|
||||||
|
|
||||||
SetTrackScoreDialog(this, item).showDialog(router)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setStatus(item: TrackItem, selection: Int) {
|
|
||||||
presenter.setStatus(item, selection)
|
|
||||||
swipe_refresh?.isRefreshing = true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setScore(item: TrackItem, score: Int) {
|
|
||||||
presenter.setScore(item, score)
|
|
||||||
swipe_refresh?.isRefreshing = true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setChaptersRead(item: TrackItem, chaptersRead: Int) {
|
|
||||||
presenter.setLastChapterRead(item, chaptersRead)
|
|
||||||
swipe_refresh?.isRefreshing = true
|
|
||||||
}
|
|
||||||
|
|
||||||
private companion object {
|
|
||||||
const val TAG_SEARCH_CONTROLLER = "track_search_controller"
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -2,8 +2,8 @@ package eu.kanade.tachiyomi.ui.manga.track
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.ui.base.holder.BaseViewHolder
|
import eu.kanade.tachiyomi.ui.base.holder.BaseViewHolder
|
||||||
|
import eu.kanade.tachiyomi.util.view.visibleIf
|
||||||
import kotlinx.android.synthetic.main.track_item.*
|
import kotlinx.android.synthetic.main.track_item.*
|
||||||
|
|
||||||
class TrackHolder(view: View, adapter: TrackAdapter) : BaseViewHolder(view) {
|
class TrackHolder(view: View, adapter: TrackAdapter) : BaseViewHolder(view) {
|
||||||
@ -11,32 +11,28 @@ class TrackHolder(view: View, adapter: TrackAdapter) : BaseViewHolder(view) {
|
|||||||
init {
|
init {
|
||||||
val listener = adapter.rowClickListener
|
val listener = adapter.rowClickListener
|
||||||
logo_container.setOnClickListener { listener.onLogoClick(adapterPosition) }
|
logo_container.setOnClickListener { listener.onLogoClick(adapterPosition) }
|
||||||
title_container.setOnClickListener { listener.onTitleClick(adapterPosition) }
|
track_set.setOnClickListener { listener.onSetClick(adapterPosition) }
|
||||||
status_container.setOnClickListener { listener.onStatusClick(adapterPosition) }
|
status_container.setOnClickListener { listener.onStatusClick(adapterPosition) }
|
||||||
chapters_container.setOnClickListener { listener.onChaptersClick(adapterPosition) }
|
chapters_container.setOnClickListener { listener.onChaptersClick(adapterPosition) }
|
||||||
score_container.setOnClickListener { listener.onScoreClick(adapterPosition) }
|
score_container.setOnClickListener { listener.onScoreClick(adapterPosition) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
@SuppressLint("SetTextI18n")
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
fun bind(item: TrackItem) {
|
fun bind(item: TrackItem) {
|
||||||
val track = item.track
|
val track = item.track
|
||||||
track_logo.setImageResource(item.service.getLogo())
|
track_logo.setImageResource(item.service.getLogo())
|
||||||
logo_container.setBackgroundColor(item.service.getLogoColor())
|
logo_container.setBackgroundColor(item.service.getLogoColor())
|
||||||
|
track_group.visibleIf(track != null)
|
||||||
if (track != null) {
|
if (track != null) {
|
||||||
track_title.setTextAppearance(itemView.context, R.style.TextAppearance_Regular_Body1_Secondary)
|
|
||||||
track_title.isAllCaps = false
|
|
||||||
track_title.text = track.title
|
|
||||||
track_chapters.text = "${track.last_chapter_read}/" +
|
track_chapters.text = "${track.last_chapter_read}/" +
|
||||||
if (track.total_chapters > 0) track.total_chapters else "-"
|
if (track.total_chapters > 0) track.total_chapters else "-"
|
||||||
track_status.text = item.service.getStatus(track.status)
|
track_status.text = item.service.getStatus(track.status)
|
||||||
track_score.text = if (track.score == 0f) "-" else item.service.displayScore(track)
|
track_score.text = if (track.score == 0f) "-" else item.service.displayScore(track)
|
||||||
} else {
|
|
||||||
track_title.setTextAppearance(itemView.context, R.style.TextAppearance_Medium_Button)
|
|
||||||
track_title.setText(R.string.action_edit)
|
|
||||||
track_chapters.text = ""
|
|
||||||
track_score.text = ""
|
|
||||||
track_status.text = ""
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setProgress(enabled: Boolean) {
|
||||||
|
progress.visibleIf(enabled)
|
||||||
|
track_logo.visibleIf(!enabled)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,130 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.manga.track
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Track
|
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
|
||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
|
||||||
import rx.Observable
|
|
||||||
import rx.Subscription
|
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
|
||||||
import rx.schedulers.Schedulers
|
|
||||||
import uy.kohesive.injekt.Injekt
|
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
|
|
||||||
|
|
||||||
class TrackPresenter(
|
|
||||||
val manga: Manga,
|
|
||||||
preferences: PreferencesHelper = Injekt.get(),
|
|
||||||
private val db: DatabaseHelper = Injekt.get(),
|
|
||||||
private val trackManager: TrackManager = Injekt.get()
|
|
||||||
) : BasePresenter<TrackController>() {
|
|
||||||
|
|
||||||
private val context = preferences.context
|
|
||||||
|
|
||||||
private var trackList: List<TrackItem> = emptyList()
|
|
||||||
|
|
||||||
private val loggedServices by lazy { trackManager.services.filter { it.isLogged } }
|
|
||||||
|
|
||||||
private var trackSubscription: Subscription? = null
|
|
||||||
|
|
||||||
private var searchSubscription: Subscription? = null
|
|
||||||
|
|
||||||
private var refreshSubscription: Subscription? = null
|
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
|
||||||
super.onCreate(savedState)
|
|
||||||
fetchTrackings()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun fetchTrackings() {
|
|
||||||
trackSubscription?.let { remove(it) }
|
|
||||||
trackSubscription = db.getTracks(manga)
|
|
||||||
.asRxObservable()
|
|
||||||
.map { tracks ->
|
|
||||||
loggedServices.map { service ->
|
|
||||||
TrackItem(tracks.find { it.sync_id == service.id }, service)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.doOnNext { trackList = it }
|
|
||||||
.subscribeLatestCache(TrackController::onNextTrackings)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun refresh() {
|
|
||||||
refreshSubscription?.let { remove(it) }
|
|
||||||
refreshSubscription = Observable.from(trackList)
|
|
||||||
.filter { it.track != null }
|
|
||||||
.concatMap { item ->
|
|
||||||
item.service.refresh(item.track!!)
|
|
||||||
.flatMap { db.insertTrack(it).asRxObservable() }
|
|
||||||
.map { item }
|
|
||||||
.onErrorReturn { item }
|
|
||||||
}
|
|
||||||
.toList()
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribeFirst({ view, _ -> view.onRefreshDone() },
|
|
||||||
TrackController::onRefreshError)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun search(query: String, service: TrackService) {
|
|
||||||
searchSubscription?.let { remove(it) }
|
|
||||||
searchSubscription = service.search(query)
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribeLatestCache(TrackController::onSearchResults,
|
|
||||||
TrackController::onSearchResultsError)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun registerTracking(item: Track?, service: TrackService) {
|
|
||||||
if (item != null) {
|
|
||||||
item.manga_id = manga.id!!
|
|
||||||
add(service.bind(item)
|
|
||||||
.flatMap { db.insertTrack(item).asRxObservable() }
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribeFirst({ view, _ -> view.onRefreshDone() },
|
|
||||||
TrackController::onRefreshError))
|
|
||||||
} else {
|
|
||||||
db.deleteTrackForManga(manga, service).executeAsBlocking()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateRemote(track: Track, service: TrackService) {
|
|
||||||
service.update(track)
|
|
||||||
.flatMap { db.insertTrack(track).asRxObservable() }
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribeFirst({ view, _ -> view.onRefreshDone() },
|
|
||||||
{ view, error ->
|
|
||||||
view.onRefreshError(error)
|
|
||||||
|
|
||||||
// Restart on error to set old values
|
|
||||||
fetchTrackings()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setStatus(item: TrackItem, index: Int) {
|
|
||||||
val track = item.track!!
|
|
||||||
track.status = item.service.getStatusList()[index]
|
|
||||||
updateRemote(track, item.service)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setScore(item: TrackItem, index: Int) {
|
|
||||||
val track = item.track!!
|
|
||||||
track.score = item.service.indexToScore(index)
|
|
||||||
updateRemote(track, item.service)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setLastChapterRead(item: TrackItem, chapterNumber: Int) {
|
|
||||||
val track = item.track!!
|
|
||||||
track.last_chapter_read = chapterNumber
|
|
||||||
updateRemote(track, item.service)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -15,11 +15,10 @@ import eu.kanade.tachiyomi.data.track.TrackManager
|
|||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||||
import kotlinx.android.synthetic.main.track_controller.*
|
import eu.kanade.tachiyomi.ui.manga.MangaDetailsPresenter
|
||||||
|
import eu.kanade.tachiyomi.ui.manga.TrackingBottomSheet
|
||||||
import eu.kanade.tachiyomi.util.lang.plusAssign
|
import eu.kanade.tachiyomi.util.lang.plusAssign
|
||||||
import kotlinx.android.synthetic.main.track_search_dialog.view.progress
|
import kotlinx.android.synthetic.main.track_search_dialog.view.*
|
||||||
import kotlinx.android.synthetic.main.track_search_dialog.view.track_search
|
|
||||||
import kotlinx.android.synthetic.main.track_search_dialog.view.track_search_list
|
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import rx.subscriptions.CompositeSubscription
|
import rx.subscriptions.CompositeSubscription
|
||||||
@ -41,17 +40,22 @@ class TrackSearchDialog : DialogController {
|
|||||||
|
|
||||||
private var searchTextSubscription: Subscription? = null
|
private var searchTextSubscription: Subscription? = null
|
||||||
|
|
||||||
private val trackController
|
private lateinit var bottomSheet: TrackingBottomSheet
|
||||||
get() = targetController as TrackController
|
//private val trackController
|
||||||
|
// get() = targetController as TrackController
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
private var wasPreviouslyTracked:Boolean = false
|
private var wasPreviouslyTracked:Boolean = false
|
||||||
|
private lateinit var presenter:MangaDetailsPresenter
|
||||||
|
|
||||||
constructor(target: TrackController, service: TrackService, wasTracked:Boolean) : super(Bundle()
|
constructor(target: TrackingBottomSheet, service: TrackService, wasTracked:Boolean) : super(Bundle()
|
||||||
.apply {
|
.apply {
|
||||||
putInt(KEY_SERVICE, service.id)
|
putInt(KEY_SERVICE, service.id)
|
||||||
}) {
|
}) {
|
||||||
wasPreviouslyTracked = wasTracked
|
wasPreviouslyTracked = wasTracked
|
||||||
targetController = target
|
bottomSheet = target
|
||||||
|
presenter = target.presenter
|
||||||
this.service = service
|
this.service = service
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,7 +101,7 @@ class TrackSearchDialog : DialogController {
|
|||||||
|
|
||||||
// Do an initial search based on the manga's title
|
// Do an initial search based on the manga's title
|
||||||
if (savedState == null) {
|
if (savedState == null) {
|
||||||
val title = trackController.presenter.manga.originalTitle()
|
val title = presenter.manga.originalTitle()
|
||||||
view.track_search.append(title)
|
view.track_search.append(title)
|
||||||
search(title)
|
search(title)
|
||||||
}
|
}
|
||||||
@ -129,7 +133,7 @@ class TrackSearchDialog : DialogController {
|
|||||||
val view = dialogView ?: return
|
val view = dialogView ?: return
|
||||||
view.progress.visibility = View.VISIBLE
|
view.progress.visibility = View.VISIBLE
|
||||||
view.track_search_list.visibility = View.INVISIBLE
|
view.track_search_list.visibility = View.INVISIBLE
|
||||||
trackController.presenter.search(query, service)
|
presenter.trackSearch(query, service)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onSearchResults(results: List<TrackSearch>) {
|
fun onSearchResults(results: List<TrackSearch>) {
|
||||||
@ -153,8 +157,10 @@ class TrackSearchDialog : DialogController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun onPositiveButtonClick() {
|
private fun onPositiveButtonClick() {
|
||||||
trackController.swipe_refresh.isRefreshing = true
|
// trackController.swipe_refresh.isRefreshing = true
|
||||||
trackController.presenter.registerTracking(selectedItem, service)
|
bottomSheet.refreshTrack(service)
|
||||||
|
presenter.registerTracking(selectedItem,
|
||||||
|
service)
|
||||||
}
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
|
@ -24,6 +24,11 @@ import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
|||||||
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
|
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
import eu.kanade.tachiyomi.util.system.ImageUtil
|
import eu.kanade.tachiyomi.util.system.ImageUtil
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import rx.Completable
|
import rx.Completable
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
@ -231,9 +236,11 @@ class ReaderPresenter(
|
|||||||
.andThen(Observable.fromCallable {
|
.andThen(Observable.fromCallable {
|
||||||
val chapterPos = chapterList.indexOf(chapter)
|
val chapterPos = chapterList.indexOf(chapter)
|
||||||
|
|
||||||
ViewerChapters(chapter,
|
ViewerChapters(
|
||||||
|
chapter,
|
||||||
chapterList.getOrNull(chapterPos - 1),
|
chapterList.getOrNull(chapterPos - 1),
|
||||||
chapterList.getOrNull(chapterPos + 1))
|
chapterList.getOrNull(chapterPos + 1)
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.doOnNext { newChapters ->
|
.doOnNext { newChapters ->
|
||||||
@ -464,9 +471,11 @@ class ReaderPresenter(
|
|||||||
notifier.onClear()
|
notifier.onClear()
|
||||||
|
|
||||||
// Pictures directory.
|
// Pictures directory.
|
||||||
val destDir = File(Environment.getExternalStorageDirectory().absolutePath +
|
val destDir = File(
|
||||||
|
Environment.getExternalStorageDirectory().absolutePath +
|
||||||
File.separator + Environment.DIRECTORY_PICTURES +
|
File.separator + Environment.DIRECTORY_PICTURES +
|
||||||
File.separator + "Tachiyomi")
|
File.separator + "Tachiyomi"
|
||||||
|
)
|
||||||
|
|
||||||
// Copy file in background.
|
// Copy file in background.
|
||||||
Observable.fromCallable { saveImage(page, destDir, manga) }
|
Observable.fromCallable { saveImage(page, destDir, manga) }
|
||||||
@ -568,27 +577,24 @@ class ReaderPresenter(
|
|||||||
|
|
||||||
val trackManager = Injekt.get<TrackManager>()
|
val trackManager = Injekt.get<TrackManager>()
|
||||||
|
|
||||||
db.getTracks(manga).asRxSingle()
|
// We wan't these to execute even if the presenter is destroyed so launch on GlobalScope
|
||||||
.flatMapCompletable { trackList ->
|
GlobalScope.launch {
|
||||||
Completable.concat(trackList.map { track ->
|
withContext(Dispatchers.IO) {
|
||||||
|
val trackList = db.getTracks(manga).executeAsBlocking()
|
||||||
|
trackList.map { track ->
|
||||||
val service = trackManager.getService(track.sync_id)
|
val service = trackManager.getService(track.sync_id)
|
||||||
if (service != null && service.isLogged && chapterRead > track.last_chapter_read) {
|
if (service != null && service.isLogged && chapterRead > track.last_chapter_read) {
|
||||||
|
try {
|
||||||
track.last_chapter_read = chapterRead
|
track.last_chapter_read = chapterRead
|
||||||
|
service.update(track)
|
||||||
// We wan't these to execute even if the presenter is destroyed and leaks
|
db.insertTrack(track).executeAsBlocking()
|
||||||
// for a while. The view can still be garbage collected.
|
} catch (e: Exception) {
|
||||||
Observable.defer { service.update(track) }
|
Timber.e(e)
|
||||||
.map { db.insertTrack(track).executeAsBlocking() }
|
}
|
||||||
.toCompletable()
|
}
|
||||||
.onErrorComplete()
|
}
|
||||||
} else {
|
|
||||||
Completable.complete()
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
.onErrorComplete()
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.subscribe()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -629,5 +635,4 @@ class ReaderPresenter(
|
|||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.subscribe()
|
.subscribe()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -5,12 +5,12 @@ import android.os.Build
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
|
||||||
import androidx.core.widget.NestedScrollView
|
|
||||||
import android.widget.CompoundButton
|
import android.widget.CompoundButton
|
||||||
import android.widget.Spinner
|
import android.widget.Spinner
|
||||||
|
import androidx.core.widget.NestedScrollView
|
||||||
import com.f2prateek.rx.preferences.Preference
|
import com.f2prateek.rx.preferences.Preference
|
||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
@ -115,7 +115,7 @@ class ReaderSettingsSheet(private val activity: ReaderActivity) : BottomSheetDia
|
|||||||
show_page_number.bindToPreference(preferences.showPageNumber())
|
show_page_number.bindToPreference(preferences.showPageNumber())
|
||||||
fullscreen.bindToPreference(preferences.fullscreen())
|
fullscreen.bindToPreference(preferences.fullscreen())
|
||||||
keepscreen.bindToPreference(preferences.keepScreenOn())
|
keepscreen.bindToPreference(preferences.keepScreenOn())
|
||||||
long_tap.bindToPreference(preferences.readWithLongTap())
|
always_show_chapter_transition.bindToPreference(preferences.alwaysShowChapterTransition())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -46,6 +46,9 @@ class PagerConfig(private val viewer: PagerViewer, preferences: PreferencesHelpe
|
|||||||
var readerTheme = 0
|
var readerTheme = 0
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
var alwaysShowChapterTransition = true
|
||||||
|
private set
|
||||||
|
|
||||||
init {
|
init {
|
||||||
preferences.readWithTapping()
|
preferences.readWithTapping()
|
||||||
.register({ tappingEnabled = it })
|
.register({ tappingEnabled = it })
|
||||||
@ -76,6 +79,9 @@ class PagerConfig(private val viewer: PagerViewer, preferences: PreferencesHelpe
|
|||||||
|
|
||||||
preferences.readerTheme()
|
preferences.readerTheme()
|
||||||
.register({ readerTheme = it })
|
.register({ readerTheme = it })
|
||||||
|
|
||||||
|
preferences.alwaysShowChapterTransition()
|
||||||
|
.register({ alwaysShowChapterTransition = it })
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unsubscribe() {
|
fun unsubscribe() {
|
||||||
|
@ -144,8 +144,10 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
|
|||||||
Timber.d("onReaderPageSelected: ${page.number}/${pages.size}")
|
Timber.d("onReaderPageSelected: ${page.number}/${pages.size}")
|
||||||
activity.onPageSelected(page)
|
activity.onPageSelected(page)
|
||||||
|
|
||||||
if (page === pages.last()) {
|
// Preload next chapter once we're within the last 3 pages of the current chapter
|
||||||
Timber.d("Request preload next chapter because we're at the last page")
|
val inPreloadRange = pages.size - page.number < 3
|
||||||
|
if (inPreloadRange) {
|
||||||
|
Timber.d("Request preload next chapter because we're at page ${page.number} of ${pages.size}")
|
||||||
adapter.nextTransition?.to?.let {
|
adapter.nextTransition?.to?.let {
|
||||||
activity.requestPreloadChapter(it)
|
activity.requestPreloadChapter(it)
|
||||||
}
|
}
|
||||||
@ -185,7 +187,8 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
|
|||||||
*/
|
*/
|
||||||
private fun setChaptersInternal(chapters: ViewerChapters) {
|
private fun setChaptersInternal(chapters: ViewerChapters) {
|
||||||
Timber.d("setChaptersInternal")
|
Timber.d("setChaptersInternal")
|
||||||
adapter.setChapters(chapters)
|
var forceTransition = config.alwaysShowChapterTransition || adapter.items.getOrNull(pager.currentItem) is ChapterTransition
|
||||||
|
adapter.setChapters(chapters, forceTransition)
|
||||||
|
|
||||||
// Layout the pager once a chapter is being set
|
// Layout the pager once a chapter is being set
|
||||||
if (pager.visibility == View.GONE) {
|
if (pager.visibility == View.GONE) {
|
||||||
|
@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.reader.viewer.pager
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
|
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
|
||||||
|
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
|
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
|
||||||
import eu.kanade.tachiyomi.widget.ViewPagerAdapter
|
import eu.kanade.tachiyomi.widget.ViewPagerAdapter
|
||||||
@ -27,7 +28,7 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
|
|||||||
* next/previous chapter to allow seamless transitions and inverting the pages if the viewer
|
* next/previous chapter to allow seamless transitions and inverting the pages if the viewer
|
||||||
* has R2L direction.
|
* has R2L direction.
|
||||||
*/
|
*/
|
||||||
fun setChapters(chapters: ViewerChapters) {
|
fun setChapters(chapters: ViewerChapters, forceTransition: Boolean) {
|
||||||
val newItems = mutableListOf<Any>()
|
val newItems = mutableListOf<Any>()
|
||||||
|
|
||||||
// Add previous chapter pages and transition.
|
// Add previous chapter pages and transition.
|
||||||
@ -39,7 +40,11 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
|
|||||||
newItems.addAll(prevPages.takeLast(2))
|
newItems.addAll(prevPages.takeLast(2))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip transition page if the chapter is loaded & current page is not a transition page
|
||||||
|
if (forceTransition || chapters.prevChapter?.state !is ReaderChapter.State.Loaded) {
|
||||||
newItems.add(ChapterTransition.Prev(chapters.currChapter, chapters.prevChapter))
|
newItems.add(ChapterTransition.Prev(chapters.currChapter, chapters.prevChapter))
|
||||||
|
}
|
||||||
|
|
||||||
// Add current chapter.
|
// Add current chapter.
|
||||||
val currPages = chapters.currChapter.pages
|
val currPages = chapters.currChapter.pages
|
||||||
@ -49,7 +54,13 @@ class PagerViewerAdapter(private val viewer: PagerViewer) : ViewPagerAdapter() {
|
|||||||
|
|
||||||
// Add next chapter transition and pages.
|
// Add next chapter transition and pages.
|
||||||
nextTransition = ChapterTransition.Next(chapters.currChapter, chapters.nextChapter)
|
nextTransition = ChapterTransition.Next(chapters.currChapter, chapters.nextChapter)
|
||||||
.also { newItems.add(it) }
|
.also {
|
||||||
|
if (forceTransition ||
|
||||||
|
chapters.nextChapter?.state !is ReaderChapter.State.Loaded) {
|
||||||
|
newItems.add(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (chapters.nextChapter != null) {
|
if (chapters.nextChapter != null) {
|
||||||
// Add at most two pages, because this chapter will be selected before the user can
|
// Add at most two pages, because this chapter will be selected before the user can
|
||||||
// swap more pages.
|
// swap more pages.
|
||||||
|
@ -6,6 +6,7 @@ import android.view.ViewGroup
|
|||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.LinearLayout
|
import android.widget.LinearLayout
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
|
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
|
||||||
|
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
|
||||||
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
|
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
|
||||||
|
|
||||||
@ -24,7 +25,7 @@ class WebtoonAdapter(val viewer: WebtoonViewer) : androidx.recyclerview.widget.R
|
|||||||
* Updates this adapter with the given [chapters]. It handles setting a few pages of the
|
* Updates this adapter with the given [chapters]. It handles setting a few pages of the
|
||||||
* next/previous chapter to allow seamless transitions.
|
* next/previous chapter to allow seamless transitions.
|
||||||
*/
|
*/
|
||||||
fun setChapters(chapters: ViewerChapters) {
|
fun setChapters(chapters: ViewerChapters, forceTransition: Boolean) {
|
||||||
val newItems = mutableListOf<Any>()
|
val newItems = mutableListOf<Any>()
|
||||||
|
|
||||||
// Add previous chapter pages and transition.
|
// Add previous chapter pages and transition.
|
||||||
@ -36,7 +37,11 @@ class WebtoonAdapter(val viewer: WebtoonViewer) : androidx.recyclerview.widget.R
|
|||||||
newItems.addAll(prevPages.takeLast(2))
|
newItems.addAll(prevPages.takeLast(2))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip transition page if the chapter is loaded & current page is not a transition page
|
||||||
|
if (forceTransition || chapters.prevChapter?.state !is ReaderChapter.State.Loaded) {
|
||||||
newItems.add(ChapterTransition.Prev(chapters.currChapter, chapters.prevChapter))
|
newItems.add(ChapterTransition.Prev(chapters.currChapter, chapters.prevChapter))
|
||||||
|
}
|
||||||
|
|
||||||
// Add current chapter.
|
// Add current chapter.
|
||||||
val currPages = chapters.currChapter.pages
|
val currPages = chapters.currChapter.pages
|
||||||
@ -45,7 +50,10 @@ class WebtoonAdapter(val viewer: WebtoonViewer) : androidx.recyclerview.widget.R
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add next chapter transition and pages.
|
// Add next chapter transition and pages.
|
||||||
|
if (forceTransition || chapters.nextChapter?.state !is ReaderChapter.State.Loaded) {
|
||||||
newItems.add(ChapterTransition.Next(chapters.currChapter, chapters.nextChapter))
|
newItems.add(ChapterTransition.Next(chapters.currChapter, chapters.nextChapter))
|
||||||
|
}
|
||||||
|
|
||||||
if (chapters.nextChapter != null) {
|
if (chapters.nextChapter != null) {
|
||||||
// Add at most two pages, because this chapter will be selected before the user can
|
// Add at most two pages, because this chapter will be selected before the user can
|
||||||
// swap more pages.
|
// swap more pages.
|
||||||
|
@ -34,6 +34,9 @@ class WebtoonConfig(preferences: PreferencesHelper = Injekt.get()) {
|
|||||||
var doubleTapAnimDuration = 500
|
var doubleTapAnimDuration = 500
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
var alwaysShowChapterTransition = true
|
||||||
|
private set
|
||||||
|
|
||||||
init {
|
init {
|
||||||
preferences.readWithTapping()
|
preferences.readWithTapping()
|
||||||
.register({ tappingEnabled = it })
|
.register({ tappingEnabled = it })
|
||||||
@ -52,6 +55,9 @@ class WebtoonConfig(preferences: PreferencesHelper = Injekt.get()) {
|
|||||||
|
|
||||||
preferences.readWithVolumeKeysInverted()
|
preferences.readWithVolumeKeysInverted()
|
||||||
.register({ volumeKeysInverted = it })
|
.register({ volumeKeysInverted = it })
|
||||||
|
|
||||||
|
preferences.alwaysShowChapterTransition()
|
||||||
|
.register({ alwaysShowChapterTransition = it })
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unsubscribe() {
|
fun unsubscribe() {
|
||||||
|
@ -142,9 +142,11 @@ class WebtoonViewer(val activity: ReaderActivity) : BaseViewer {
|
|||||||
Timber.d("onPageSelected: ${page.number}/${pages.size}")
|
Timber.d("onPageSelected: ${page.number}/${pages.size}")
|
||||||
activity.onPageSelected(page)
|
activity.onPageSelected(page)
|
||||||
|
|
||||||
if (page === pages.last()) {
|
// Preload next chapter once we're within the last 3 pages of the current chapter
|
||||||
Timber.d("Request preload next chapter because we're at the last page")
|
val inPreloadRange = pages.size - page.number < 3
|
||||||
val transition = adapter.items.getOrNull(position + 1) as? ChapterTransition.Next
|
if (inPreloadRange) {
|
||||||
|
Timber.d("Request preload next chapter because we're at page ${page.number} of ${pages.size}")
|
||||||
|
val transition = adapter.items.getOrNull(pages.size + 1) as? ChapterTransition.Next
|
||||||
if (transition?.to != null) {
|
if (transition?.to != null) {
|
||||||
activity.requestPreloadChapter(transition.to)
|
activity.requestPreloadChapter(transition.to)
|
||||||
}
|
}
|
||||||
@ -172,7 +174,8 @@ class WebtoonViewer(val activity: ReaderActivity) : BaseViewer {
|
|||||||
*/
|
*/
|
||||||
override fun setChapters(chapters: ViewerChapters) {
|
override fun setChapters(chapters: ViewerChapters) {
|
||||||
Timber.d("setChapters")
|
Timber.d("setChapters")
|
||||||
adapter.setChapters(chapters)
|
var forceTransition = config.alwaysShowChapterTransition || currentPage is ChapterTransition
|
||||||
|
adapter.setChapters(chapters, forceTransition)
|
||||||
|
|
||||||
if (recycler.visibility == View.GONE) {
|
if (recycler.visibility == View.GONE) {
|
||||||
Timber.d("Recycler first layout")
|
Timber.d("Recycler first layout")
|
||||||
|
@ -11,7 +11,6 @@ import androidx.core.content.ContextCompat
|
|||||||
import androidx.preference.PreferenceScreen
|
import androidx.preference.PreferenceScreen
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
import com.afollestad.materialdialogs.list.listItemsSingleChoice
|
import com.afollestad.materialdialogs.list.listItemsSingleChoice
|
||||||
import com.bluelinelabs.conductor.RouterTransaction
|
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
@ -34,6 +33,13 @@ class SettingsDownloadController : SettingsController() {
|
|||||||
override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) {
|
override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) {
|
||||||
titleRes = R.string.pref_category_downloads
|
titleRes = R.string.pref_category_downloads
|
||||||
|
|
||||||
|
preference {
|
||||||
|
titleRes = R.string.label_download_queue
|
||||||
|
onClick {
|
||||||
|
router.pushController(DownloadController().withFadeTransaction())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
preference {
|
preference {
|
||||||
key = Keys.downloadsDirectory
|
key = Keys.downloadsDirectory
|
||||||
titleRes = R.string.pref_download_directory
|
titleRes = R.string.pref_download_directory
|
||||||
|
@ -45,11 +45,11 @@ class SettingsGeneralController : SettingsController() {
|
|||||||
intListPreference(activity) {
|
intListPreference(activity) {
|
||||||
key = Keys.theme
|
key = Keys.theme
|
||||||
titleRes = R.string.pref_theme
|
titleRes = R.string.pref_theme
|
||||||
entriesRes = arrayOf(R.string.light_theme, R.string.white_theme, R.string.dark_theme,
|
entriesRes = arrayOf(R.string.white_theme, R.string.light_theme, R.string.dark_theme,
|
||||||
R.string.amoled_theme, R.string.darkblue_theme,
|
R.string.amoled_theme, R.string.darkblue_theme,
|
||||||
R.string.system_theme, R.string.sysyem_white_theme, R.string.system_amoled_theme, R.string
|
R.string.sysyem_white_theme, R.string.system_theme, R.string.system_amoled_theme,
|
||||||
.system_darkblue_theme)
|
R.string.system_darkblue_theme)
|
||||||
entryValues = listOf(1, 8, 2, 3, 4, 5, 9, 6, 7)
|
entryValues = listOf(8, 1, 2, 3, 4, 9, 5, 6, 7)
|
||||||
defaultValue = 9
|
defaultValue = 9
|
||||||
|
|
||||||
onChange {
|
onChange {
|
||||||
|
@ -8,7 +8,6 @@ import com.bluelinelabs.conductor.Controller
|
|||||||
import eu.kanade.tachiyomi.BuildConfig
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||||
import eu.kanade.tachiyomi.ui.extension.ExtensionController
|
|
||||||
import eu.kanade.tachiyomi.ui.migration.MigrationController
|
import eu.kanade.tachiyomi.ui.migration.MigrationController
|
||||||
import eu.kanade.tachiyomi.util.system.getResourceColor
|
import eu.kanade.tachiyomi.util.system.getResourceColor
|
||||||
import eu.kanade.tachiyomi.util.system.openInBrowser
|
import eu.kanade.tachiyomi.util.system.openInBrowser
|
||||||
@ -24,13 +23,6 @@ class SettingsMainController : SettingsController() {
|
|||||||
|
|
||||||
val tintColor = context.getResourceColor(R.attr.colorAccent)
|
val tintColor = context.getResourceColor(R.attr.colorAccent)
|
||||||
|
|
||||||
extensionPreference {
|
|
||||||
iconRes = R.drawable.ic_extension_black_24dp
|
|
||||||
iconTint = tintColor
|
|
||||||
titleRes = R.string.label_extensions
|
|
||||||
onClick { navigateTo(ExtensionController()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
preference {
|
preference {
|
||||||
iconRes = R.drawable.ic_tune_white_24dp
|
iconRes = R.drawable.ic_tune_white_24dp
|
||||||
iconTint = tintColor
|
iconTint = tintColor
|
||||||
|
@ -87,6 +87,12 @@ class SettingsReaderController : SettingsController() {
|
|||||||
defaultValue = false
|
defaultValue = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
switchPreference {
|
||||||
|
key = Keys.alwaysShowChapterTransition
|
||||||
|
titleRes = R.string.pref_always_show_chapter_transition
|
||||||
|
defaultValue = true
|
||||||
|
}
|
||||||
|
|
||||||
preferenceCategory {
|
preferenceCategory {
|
||||||
titleRes = R.string.pager_viewer
|
titleRes = R.string.pager_viewer
|
||||||
|
|
||||||
|
@ -50,7 +50,7 @@ class SettingsTrackingController : SettingsController(),
|
|||||||
}
|
}
|
||||||
trackPreference(trackManager.kitsu) {
|
trackPreference(trackManager.kitsu) {
|
||||||
onClick {
|
onClick {
|
||||||
val dialog = TrackLoginDialog(trackManager.kitsu)
|
val dialog = TrackLoginDialog(trackManager.kitsu, context.getString(R.string.email))
|
||||||
dialog.targetController = this@SettingsTrackingController
|
dialog.targetController = this@SettingsTrackingController
|
||||||
dialog.showDialog(router)
|
dialog.showDialog(router)
|
||||||
}
|
}
|
||||||
|
@ -2,21 +2,25 @@ package eu.kanade.tachiyomi.ui.setting.track
|
|||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import android.view.Gravity.CENTER
|
import android.view.Gravity.CENTER
|
||||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.ProgressBar
|
import android.widget.ProgressBar
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import rx.schedulers.Schedulers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class AnilistLoginActivity : AppCompatActivity() {
|
class AnilistLoginActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private val trackManager: TrackManager by injectLazy()
|
private val trackManager: TrackManager by injectLazy()
|
||||||
|
|
||||||
|
private val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
override fun onCreate(savedState: Bundle?) {
|
||||||
super.onCreate(savedState)
|
super.onCreate(savedState)
|
||||||
|
|
||||||
@ -26,14 +30,10 @@ class AnilistLoginActivity : AppCompatActivity() {
|
|||||||
val regex = "(?:access_token=)(.*?)(?:&)".toRegex()
|
val regex = "(?:access_token=)(.*?)(?:&)".toRegex()
|
||||||
val matchResult = regex.find(intent.data?.fragment.toString())
|
val matchResult = regex.find(intent.data?.fragment.toString())
|
||||||
if (matchResult?.groups?.get(1) != null) {
|
if (matchResult?.groups?.get(1) != null) {
|
||||||
|
scope.launch {
|
||||||
trackManager.aniList.login(matchResult.groups[1]!!.value)
|
trackManager.aniList.login(matchResult.groups[1]!!.value)
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe({
|
|
||||||
returnToSettings()
|
returnToSettings()
|
||||||
}, {
|
}
|
||||||
returnToSettings()
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
trackManager.aniList.logout()
|
trackManager.aniList.logout()
|
||||||
returnToSettings()
|
returnToSettings()
|
||||||
@ -47,5 +47,4 @@ class AnilistLoginActivity : AppCompatActivity() {
|
|||||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -2,13 +2,18 @@ package eu.kanade.tachiyomi.ui.setting.track
|
|||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import android.view.Gravity.CENTER
|
import android.view.Gravity.CENTER
|
||||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.ProgressBar
|
import android.widget.ProgressBar
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
@ -17,6 +22,8 @@ class BangumiLoginActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
private val trackManager: TrackManager by injectLazy()
|
private val trackManager: TrackManager by injectLazy()
|
||||||
|
|
||||||
|
private val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
override fun onCreate(savedState: Bundle?) {
|
||||||
super.onCreate(savedState)
|
super.onCreate(savedState)
|
||||||
|
|
||||||
@ -25,14 +32,10 @@ class BangumiLoginActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
val code = intent.data?.getQueryParameter("code")
|
val code = intent.data?.getQueryParameter("code")
|
||||||
if (code != null) {
|
if (code != null) {
|
||||||
|
scope.launch {
|
||||||
trackManager.bangumi.login(code)
|
trackManager.bangumi.login(code)
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe({
|
|
||||||
returnToSettings()
|
returnToSettings()
|
||||||
}, {
|
}
|
||||||
returnToSettings()
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
trackManager.bangumi.logout()
|
trackManager.bangumi.logout()
|
||||||
returnToSettings()
|
returnToSettings()
|
||||||
@ -46,5 +49,4 @@ class BangumiLoginActivity : AppCompatActivity() {
|
|||||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -2,21 +2,25 @@ package eu.kanade.tachiyomi.ui.setting.track
|
|||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import android.view.Gravity.CENTER
|
import android.view.Gravity.CENTER
|
||||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.ProgressBar
|
import android.widget.ProgressBar
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import rx.schedulers.Schedulers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class ShikimoriLoginActivity : AppCompatActivity() {
|
class ShikimoriLoginActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private val trackManager: TrackManager by injectLazy()
|
private val trackManager: TrackManager by injectLazy()
|
||||||
|
|
||||||
|
private val scope = CoroutineScope(Job() + Dispatchers.Main)
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
override fun onCreate(savedState: Bundle?) {
|
||||||
super.onCreate(savedState)
|
super.onCreate(savedState)
|
||||||
|
|
||||||
@ -25,14 +29,10 @@ class ShikimoriLoginActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
val code = intent.data?.getQueryParameter("code")
|
val code = intent.data?.getQueryParameter("code")
|
||||||
if (code != null) {
|
if (code != null) {
|
||||||
|
scope.launch {
|
||||||
trackManager.shikimori.login(code)
|
trackManager.shikimori.login(code)
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe({
|
|
||||||
returnToSettings()
|
returnToSettings()
|
||||||
}, {
|
}
|
||||||
returnToSettings()
|
|
||||||
})
|
|
||||||
} else {
|
} else {
|
||||||
trackManager.shikimori.logout()
|
trackManager.shikimori.logout()
|
||||||
returnToSettings()
|
returnToSettings()
|
||||||
@ -46,5 +46,4 @@ class ShikimoriLoginActivity : AppCompatActivity() {
|
|||||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
package eu.kanade.tachiyomi.util.system
|
||||||
|
|
||||||
|
import android.webkit.WebView
|
||||||
|
|
||||||
|
private val WEBVIEW_UA_VERSION_REGEX by lazy {
|
||||||
|
Regex(""".*Chrome/(\d+)\..*""")
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val MINIMUM_WEBVIEW_VERSION = 70
|
||||||
|
|
||||||
|
fun WebView.isOutdated(): Boolean {
|
||||||
|
return getWebviewMajorVersion(this) < MINIMUM_WEBVIEW_VERSION
|
||||||
|
}
|
||||||
|
|
||||||
|
// Based on https://stackoverflow.com/a/29218966
|
||||||
|
private fun getWebviewMajorVersion(webview: WebView): Int {
|
||||||
|
val originalUA: String = webview.settings.userAgentString
|
||||||
|
|
||||||
|
// Next call to getUserAgentString() will get us the default
|
||||||
|
webview.settings.userAgentString = null
|
||||||
|
|
||||||
|
val uaRegexMatch = WEBVIEW_UA_VERSION_REGEX.matchEntire(webview.settings.userAgentString)
|
||||||
|
val webViewVersion: Int = if (uaRegexMatch != null && uaRegexMatch.groupValues.size > 1) {
|
||||||
|
uaRegexMatch.groupValues[1].toInt()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Revert to original UA string
|
||||||
|
webview.settings.userAgentString = originalUA
|
||||||
|
|
||||||
|
return webViewVersion
|
||||||
|
}
|
@ -169,7 +169,7 @@ inline val View.marginLeft: Int
|
|||||||
|
|
||||||
object RecyclerWindowInsetsListener : View.OnApplyWindowInsetsListener {
|
object RecyclerWindowInsetsListener : View.OnApplyWindowInsetsListener {
|
||||||
override fun onApplyWindowInsets(v: View, insets: WindowInsets): WindowInsets {
|
override fun onApplyWindowInsets(v: View, insets: WindowInsets): WindowInsets {
|
||||||
v.setPadding(0,0,0,insets.systemWindowInsetBottom)
|
v.updatePaddingRelative(bottom = insets.systemWindowInsetBottom)
|
||||||
//v.updatePaddingRelative(bottom = v.paddingBottom + insets.systemWindowInsetBottom)
|
//v.updatePaddingRelative(bottom = v.paddingBottom + insets.systemWindowInsetBottom)
|
||||||
return insets
|
return insets
|
||||||
}
|
}
|
||||||
@ -294,10 +294,12 @@ data class ViewPaddingState(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
fun Controller.setOnQueryTextChangeListener(searchView: SearchView, f: (text: String?) -> Boolean) {
|
fun Controller.setOnQueryTextChangeListener(searchView: SearchView, onlyOnSubmit:Boolean = false,
|
||||||
|
f: (text: String?) -> Boolean) {
|
||||||
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||||
override fun onQueryTextChange(newText: String?): Boolean {
|
override fun onQueryTextChange(newText: String?): Boolean {
|
||||||
if (router.backstack.lastOrNull()?.controller() == this@setOnQueryTextChangeListener) {
|
if (!onlyOnSubmit && router.backstack.lastOrNull()?.controller() ==
|
||||||
|
this@setOnQueryTextChangeListener) {
|
||||||
return f(newText)
|
return f(newText)
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@ -312,36 +314,40 @@ fun Controller.setOnQueryTextChangeListener(searchView: SearchView, f: (text: St
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Controller.scrollViewWith(recycler: RecyclerView,
|
fun Controller.scrollViewWith(
|
||||||
|
recycler: RecyclerView,
|
||||||
padBottom: Boolean = false,
|
padBottom: Boolean = false,
|
||||||
swipeRefreshLayout: SwipeRefreshLayout? = null,
|
swipeRefreshLayout: SwipeRefreshLayout? = null,
|
||||||
f: ((WindowInsets) -> Unit)? = null) {
|
afterInsets: ((WindowInsets) -> Unit)? = null) {
|
||||||
var statusBarHeight = -1
|
var statusBarHeight = -1
|
||||||
activity!!.appbar.y = 0f
|
activity?.appbar?.y = 0f
|
||||||
recycler.doOnApplyWindowInsets { view, insets, _ ->
|
|
||||||
val attrsArray = intArrayOf(android.R.attr.actionBarSize)
|
val attrsArray = intArrayOf(android.R.attr.actionBarSize)
|
||||||
val array = view.context.obtainStyledAttributes(attrsArray)
|
val array = recycler.context.obtainStyledAttributes(attrsArray)
|
||||||
val headerHeight = insets.systemWindowInsetTop + array.getDimensionPixelSize(0, 0)
|
val appBarHeight = array.getDimensionPixelSize(0, 0)
|
||||||
|
array.recycle()
|
||||||
|
recycler.doOnApplyWindowInsets { view, insets, _ ->
|
||||||
|
val headerHeight = insets.systemWindowInsetTop + appBarHeight
|
||||||
view.updatePaddingRelative(
|
view.updatePaddingRelative(
|
||||||
top = headerHeight,
|
top = headerHeight,
|
||||||
bottom = if (padBottom) insets.systemWindowInsetBottom else 0
|
bottom = if (padBottom) insets.systemWindowInsetBottom else view.paddingBottom
|
||||||
|
)
|
||||||
|
swipeRefreshLayout?.setProgressViewOffset(
|
||||||
|
false, headerHeight + (-60).dpToPx, headerHeight
|
||||||
)
|
)
|
||||||
swipeRefreshLayout?.setProgressViewOffset(false, headerHeight + (-60).dpToPx,
|
|
||||||
headerHeight + 10.dpToPx)
|
|
||||||
statusBarHeight = insets.systemWindowInsetTop
|
statusBarHeight = insets.systemWindowInsetTop
|
||||||
array.recycle()
|
afterInsets?.invoke(insets)
|
||||||
f?.invoke(insets)
|
|
||||||
}
|
}
|
||||||
recycler.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
recycler.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||||
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
|
||||||
super.onScrolled(recyclerView, dx, dy)
|
super.onScrolled(recyclerView, dx, dy)
|
||||||
if (router.backstack.lastOrNull()?.controller() == this@scrollViewWith &&
|
if (router?.backstack?.lastOrNull()?.controller() == this@scrollViewWith &&
|
||||||
statusBarHeight > -1 &&
|
statusBarHeight > -1 &&
|
||||||
|
activity != null &&
|
||||||
activity!!.appbar.height > 0) {
|
activity!!.appbar.height > 0) {
|
||||||
activity!!.appbar.y -= dy
|
activity!!.appbar.y -= dy
|
||||||
activity!!.appbar.y = clamp(
|
activity!!.appbar.y = clamp(
|
||||||
activity!!.appbar.y,
|
activity!!.appbar.y,
|
||||||
-activity!!.appbar.height.toFloat(),// + statusBarHeight,
|
-activity!!.appbar.height.toFloat(),
|
||||||
0f
|
0f
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -350,8 +356,8 @@ fun Controller.scrollViewWith(recycler: RecyclerView,
|
|||||||
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
|
||||||
super.onScrollStateChanged(recyclerView, newState)
|
super.onScrollStateChanged(recyclerView, newState)
|
||||||
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
|
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
|
||||||
if (router.backstack.lastOrNull()?.controller() == this@scrollViewWith &&
|
if (router?.backstack?.lastOrNull()?.controller() == this@scrollViewWith &&
|
||||||
statusBarHeight > -1 &&
|
statusBarHeight > -1 && activity != null &&
|
||||||
activity!!.appbar.height > 0) {
|
activity!!.appbar.height > 0) {
|
||||||
val halfWay = abs((-activity!!.appbar.height.toFloat()) / 2)
|
val halfWay = abs((-activity!!.appbar.height.toFloat()) / 2)
|
||||||
val shortAnimationDuration = resources?.getInteger(
|
val shortAnimationDuration = resources?.getInteger(
|
||||||
|
@ -28,7 +28,7 @@ class ExtensionPreference @JvmOverloads constructor(context: Context, attrs: Att
|
|||||||
val updates = Injekt.get<PreferencesHelper>().extensionUpdatesCount().getOrDefault()
|
val updates = Injekt.get<PreferencesHelper>().extensionUpdatesCount().getOrDefault()
|
||||||
if (updates > 0) {
|
if (updates > 0) {
|
||||||
extUpdateText.text = context.resources.getQuantityString(R.plurals
|
extUpdateText.text = context.resources.getQuantityString(R.plurals
|
||||||
.extensions_updates_available, updates, updates)
|
.updates_available, updates, updates)
|
||||||
extUpdateText.visible()
|
extUpdateText.visible()
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
@ -13,11 +13,18 @@ import eu.kanade.tachiyomi.R
|
|||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||||
import eu.kanade.tachiyomi.widget.SimpleTextWatcher
|
import eu.kanade.tachiyomi.widget.SimpleTextWatcher
|
||||||
import kotlinx.android.synthetic.main.pref_account_login.view.*
|
import kotlinx.android.synthetic.main.pref_account_login.view.login
|
||||||
|
import kotlinx.android.synthetic.main.pref_account_login.view.password
|
||||||
|
import kotlinx.android.synthetic.main.pref_account_login.view.show_password
|
||||||
|
import kotlinx.android.synthetic.main.pref_account_login.view.username_label
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
abstract class LoginDialogPreference(bundle: Bundle? = null) : DialogController(bundle) {
|
abstract class LoginDialogPreference(private val usernameLabel: String? = null, bundle: Bundle? = null) :
|
||||||
|
DialogController(bundle), CoroutineScope {
|
||||||
|
|
||||||
var v: View? = null
|
var v: View? = null
|
||||||
private set
|
private set
|
||||||
@ -53,6 +60,10 @@ abstract class LoginDialogPreference(bundle: Bundle? = null) : DialogController(
|
|||||||
password.transformationMethod = PasswordTransformationMethod()
|
password.transformationMethod = PasswordTransformationMethod()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!usernameLabel.isNullOrEmpty()) {
|
||||||
|
username_label.text = usernameLabel
|
||||||
|
}
|
||||||
|
|
||||||
login.setMode(ActionProcessButton.Mode.ENDLESS)
|
login.setMode(ActionProcessButton.Mode.ENDLESS)
|
||||||
login.setOnClickListener { checkLogin() }
|
login.setOnClickListener { checkLogin() }
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ import rx.schedulers.Schedulers
|
|||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
class SourceLoginDialog(bundle: Bundle? = null) : LoginDialogPreference(bundle) {
|
class SourceLoginDialog(bundle: Bundle? = null) : LoginDialogPreference(bundle = bundle) {
|
||||||
|
|
||||||
private val source = Injekt.get<SourceManager>().get(args.getLong("key")) as LoginSource
|
private val source = Injekt.get<SourceManager>().get(args.getLong("key")) as LoginSource
|
||||||
|
|
||||||
|
@ -6,22 +6,25 @@ import eu.kanade.tachiyomi.R
|
|||||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||||
import eu.kanade.tachiyomi.data.track.TrackService
|
import eu.kanade.tachiyomi.data.track.TrackService
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import kotlinx.android.synthetic.main.pref_account_login.view.dialog_title
|
import kotlinx.android.synthetic.main.pref_account_login.view.*
|
||||||
import kotlinx.android.synthetic.main.pref_account_login.view.login
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.android.synthetic.main.pref_account_login.view.password
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.android.synthetic.main.pref_account_login.view.username
|
import kotlinx.coroutines.withContext
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
|
||||||
import rx.schedulers.Schedulers
|
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
|
||||||
class TrackLoginDialog(bundle: Bundle? = null) : LoginDialogPreference(bundle) {
|
class TrackLoginDialog(usernameLabel: String? = null, bundle: Bundle? = null) :
|
||||||
|
LoginDialogPreference(usernameLabel, bundle) {
|
||||||
|
|
||||||
private val service = Injekt.get<TrackManager>().getService(args.getInt("key"))!!
|
private val service = Injekt.get<TrackManager>().getService(args.getInt("key"))!!
|
||||||
|
|
||||||
override var canLogout = true
|
override var canLogout = true
|
||||||
|
|
||||||
constructor(service: TrackService) : this(Bundle().apply { putInt("key", service.id) })
|
constructor(service: TrackService) : this(service, null)
|
||||||
|
|
||||||
|
constructor(service: TrackService, usernameLabel: String?) :
|
||||||
|
this(usernameLabel, Bundle().apply { putInt("key", service.id) })
|
||||||
|
|
||||||
override fun setCredentialsOnView(view: View) = with(view) {
|
override fun setCredentialsOnView(view: View) = with(view) {
|
||||||
dialog_title.text = context.getString(R.string.login_title, service.name)
|
dialog_title.text = context.getString(R.string.login_title, service.name)
|
||||||
@ -29,6 +32,9 @@ class TrackLoginDialog(bundle: Bundle? = null) : LoginDialogPreference(bundle) {
|
|||||||
password.setText(service.getPassword())
|
password.setText(service.getPassword())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override val coroutineContext: CoroutineContext
|
||||||
|
get() = TODO("Not yet implemented")
|
||||||
|
|
||||||
override fun checkLogin() {
|
override fun checkLogin() {
|
||||||
requestSubscription?.unsubscribe()
|
requestSubscription?.unsubscribe()
|
||||||
|
|
||||||
@ -40,17 +46,21 @@ class TrackLoginDialog(bundle: Bundle? = null) : LoginDialogPreference(bundle) {
|
|||||||
val user = username.text.toString()
|
val user = username.text.toString()
|
||||||
val pass = password.text.toString()
|
val pass = password.text.toString()
|
||||||
|
|
||||||
requestSubscription = service.login(user, pass)
|
launch {
|
||||||
.subscribeOn(Schedulers.io())
|
try {
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
withContext(Dispatchers.IO) {
|
||||||
.subscribe({
|
service.login(user, pass)
|
||||||
|
}
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
dialog?.dismiss()
|
dialog?.dismiss()
|
||||||
context.toast(R.string.login_success)
|
context.toast(R.string.login_success)
|
||||||
}, { error ->
|
}
|
||||||
|
} catch (error: Exception) {
|
||||||
login.progress = -1
|
login.progress = -1
|
||||||
login.setText(R.string.unknown_error)
|
login.setText(R.string.unknown_error)
|
||||||
error.message?.let { context.toast(it) }
|
error.message?.let { context.toast(it) }
|
||||||
})
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,5 +79,4 @@ class TrackLoginDialog(bundle: Bundle? = null) : LoginDialogPreference(bundle) {
|
|||||||
interface Listener {
|
interface Listener {
|
||||||
fun trackDialogClosed(service: TrackService)
|
fun trackDialogClosed(service: TrackService)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
6
app/src/main/res/color/btn_bg_primary_selector.xml
Normal file
6
app/src/main/res/color/btn_bg_primary_selector.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:state_enabled="false"
|
||||||
|
android:alpha="0.12" android:color="?attr/colorOnSurface" />
|
||||||
|
<item android:color="?colorPrimary" />
|
||||||
|
</selector>
|
6
app/src/main/res/color/mtrl_btn_bg_selector.xml
Normal file
6
app/src/main/res/color/mtrl_btn_bg_selector.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:state_enabled="false"
|
||||||
|
android:alpha="0.12" android:color="?attr/colorOnSurface" />
|
||||||
|
<item android:color="?colorAccent" />
|
||||||
|
</selector>
|
@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:color="@android:color/white" android:state_enabled="true"/>
|
||||||
|
<item android:alpha="0.38" android:color="?attr/colorOnSurface"/>
|
||||||
|
</selector>
|
@ -2,6 +2,7 @@
|
|||||||
android:width="24dp"
|
android:width="24dp"
|
||||||
android:height="24dp"
|
android:height="24dp"
|
||||||
android:viewportWidth="24.0"
|
android:viewportWidth="24.0"
|
||||||
|
android:tint="?actionBarTintColor"
|
||||||
android:viewportHeight="24.0">
|
android:viewportHeight="24.0">
|
||||||
<path
|
<path
|
||||||
android:fillColor="#FF000000"
|
android:fillColor="#FF000000"
|
||||||
|
9
app/src/main/res/layout/auto_ext_checkbox.xml
Normal file
9
app/src/main/res/layout/auto_ext_checkbox.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<com.google.android.material.checkbox.MaterialCheckBox
|
||||||
|
android:id="@+id/auto_checkbox"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/action_auto_check_extensions">
|
||||||
|
|
||||||
|
</com.google.android.material.checkbox.MaterialCheckBox>
|
@ -1,15 +1,44 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<FrameLayout
|
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content">
|
android:layout_height="match_parent"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/frame_layout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="?android:attr/colorBackground">
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/recycler"
|
android:id="@+id/recycler"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="match_parent"
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
|
android:layout_marginBottom="30dp"
|
||||||
|
android:paddingBottom="20dp"
|
||||||
tools:listitem="@layout/catalogue_main_controller_card" />
|
tools:listitem="@layout/catalogue_main_controller_card" />
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/shadow"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:alpha="0.5"
|
||||||
|
android:background="@drawable/shape_gradient_top_shadow"
|
||||||
|
android:paddingBottom="10dp"
|
||||||
|
app:layout_anchorGravity="top"
|
||||||
|
app:layout_anchor="@id/ext_bottom_sheet" />
|
||||||
|
<!-- Adding bottom sheet after main content -->
|
||||||
|
<include layout="@layout/extensions_bottom_sheet"/>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/shadow2"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="8dp"
|
||||||
|
android:layout_gravity="bottom"
|
||||||
|
android:alpha="0.25"
|
||||||
|
android:background="@drawable/shape_gradient_top_shadow" />
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||||
|
@ -0,0 +1,43 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:id="@+id/card"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="@dimen/material_component_lists_two_line_height"
|
||||||
|
android:background="?attr/selectable_list_drawable">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/title"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:paddingStart="0dp"
|
||||||
|
android:paddingEnd="8dp"
|
||||||
|
android:textAppearance="@style/TextAppearance.MaterialComponents.Body1"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/source_browse"
|
||||||
|
tools:text="Source title" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/source_browse"
|
||||||
|
style="@style/Theme.Widget.Button.Borderless.Small"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/update_check_look_for_updates"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"/>
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
</FrameLayout>
|
@ -2,13 +2,14 @@
|
|||||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/chapter_layout"
|
||||||
android:layout_width="fill_parent"
|
android:layout_width="fill_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="?attr/selectable_list_drawable">
|
android:background="?attr/selectable_list_drawable">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/chapter_title"
|
android:id="@+id/chapter_title"
|
||||||
style="@style/TextAppearance.Regular.Body1"
|
style="@style/TextAppearance.MaterialComponents.Body2"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="16dp"
|
android:layout_marginStart="16dp"
|
||||||
@ -23,7 +24,7 @@
|
|||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/chapter_scanlator"
|
android:id="@+id/chapter_scanlator"
|
||||||
style="@style/TextAppearance.Regular.Caption.Hint"
|
style="@style/TextAppearance.MaterialComponents.Caption"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="16dp"
|
android:layout_marginStart="16dp"
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user