Merge remote-tracking branch 'upstream/master' into Automatic_Reader_Background
This commit is contained in:
commit
b4b78e0f6b
2
.github/FUNDING.yml
vendored
Normal file
2
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
github: inorichi
|
||||
ko_fi: inorichi
|
@ -11,7 +11,7 @@ Tachiyomi is a free and open source manga reader for Android.
|
||||
## Features
|
||||
|
||||
Features include:
|
||||
* Online reading from sources such as KissManga, MangaFox, [and more](https://github.com/inorichi/tachiyomi-extensions)
|
||||
* Online reading from sources such as KissManga, MangaDex, [and more](https://github.com/inorichi/tachiyomi-extensions)
|
||||
* Local reading of downloaded manga
|
||||
* Configurable reader with multiple viewers, reading directions and other settings
|
||||
* [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), and [Kitsu](https://kitsu.io/explore/anime) support
|
||||
|
@ -62,8 +62,8 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".ui.setting.ShikomoriLoginActivity"
|
||||
android:label="Shikomori">
|
||||
android:name=".ui.setting.ShikimoriLoginActivity"
|
||||
android:label="Shikimori">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
|
@ -82,6 +82,11 @@ interface MangaQueries : DbProvider {
|
||||
.withPutResolver(MangaViewerPutResolver())
|
||||
.prepare()
|
||||
|
||||
fun updateMangaTitle(manga: Manga) = db.put()
|
||||
.`object`(manga)
|
||||
.withPutResolver(MangaTitlePutResolver())
|
||||
.prepare()
|
||||
|
||||
fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare()
|
||||
|
||||
fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare()
|
||||
|
@ -0,0 +1,32 @@
|
||||
package eu.kanade.tachiyomi.data.database.resolvers
|
||||
|
||||
import android.content.ContentValues
|
||||
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
||||
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||
import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||
|
||||
class MangaTitlePutResolver : PutResolver<Manga>() {
|
||||
|
||||
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
|
||||
val updateQuery = mapToUpdateQuery(manga)
|
||||
val contentValues = mapToContentValues(manga)
|
||||
|
||||
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
|
||||
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||
}
|
||||
|
||||
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_ID} = ?")
|
||||
.whereArgs(manga.id)
|
||||
.build()
|
||||
|
||||
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
||||
put(MangaTable.COL_TITLE, manga.title)
|
||||
}
|
||||
|
||||
}
|
@ -20,10 +20,10 @@ class MangaViewerPutResolver : PutResolver<Manga>() {
|
||||
}
|
||||
|
||||
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_ID} = ?")
|
||||
.whereArgs(manga.id)
|
||||
.build()
|
||||
.table(MangaTable.TABLE)
|
||||
.where("${MangaTable.COL_ID} = ?")
|
||||
.whereArgs(manga.id)
|
||||
.build()
|
||||
|
||||
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
|
||||
put(MangaTable.COL_VIEWER, manga.viewer)
|
||||
|
@ -29,6 +29,8 @@ object PreferenceKeys {
|
||||
|
||||
const val colorFilterValue = "color_filter_value"
|
||||
|
||||
const val colorFilterMode = "color_filter_mode"
|
||||
|
||||
const val defaultViewer = "pref_default_viewer_key"
|
||||
|
||||
const val imageScaleType = "pref_image_scale_type_key"
|
||||
|
@ -57,6 +57,8 @@ class PreferencesHelper(val context: Context) {
|
||||
|
||||
fun colorFilterValue() = rxPrefs.getInteger(Keys.colorFilterValue, 0)
|
||||
|
||||
fun colorFilterMode() = rxPrefs.getInteger(Keys.colorFilterMode, 0)
|
||||
|
||||
fun defaultViewer() = prefs.getInt(Keys.defaultViewer, 1)
|
||||
|
||||
fun imageScaleType() = rxPrefs.getInteger(Keys.imageScaleType, 1)
|
||||
|
@ -4,7 +4,7 @@ import android.content.Context
|
||||
import eu.kanade.tachiyomi.data.track.anilist.Anilist
|
||||
import eu.kanade.tachiyomi.data.track.kitsu.Kitsu
|
||||
import eu.kanade.tachiyomi.data.track.myanimelist.Myanimelist
|
||||
import eu.kanade.tachiyomi.data.track.shikomori.Shikomori
|
||||
import eu.kanade.tachiyomi.data.track.shikimori.Shikimori
|
||||
|
||||
class TrackManager(private val context: Context) {
|
||||
|
||||
@ -12,7 +12,7 @@ class TrackManager(private val context: Context) {
|
||||
const val MYANIMELIST = 1
|
||||
const val ANILIST = 2
|
||||
const val KITSU = 3
|
||||
const val SHIKOMORI = 4
|
||||
const val SHIKIMORI = 4
|
||||
}
|
||||
|
||||
val myAnimeList = Myanimelist(context, MYANIMELIST)
|
||||
@ -21,9 +21,9 @@ class TrackManager(private val context: Context) {
|
||||
|
||||
val kitsu = Kitsu(context, KITSU)
|
||||
|
||||
val shikomori = Shikomori(context, SHIKOMORI)
|
||||
val shikimori = Shikimori(context, SHIKIMORI)
|
||||
|
||||
val services = listOf(myAnimeList, aniList, kitsu, shikomori)
|
||||
val services = listOf(myAnimeList, aniList, kitsu, shikimori)
|
||||
|
||||
fun getService(id: Int) = services.find { it.id == id }
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
package eu.kanade.tachiyomi.data.track.shikomori
|
||||
package eu.kanade.tachiyomi.data.track.shikimori
|
||||
|
||||
data class OAuth(
|
||||
val access_token: String,
|
@ -1,7 +1,8 @@
|
||||
package eu.kanade.tachiyomi.data.track.shikomori
|
||||
package eu.kanade.tachiyomi.data.track.shikimori
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.util.Log
|
||||
import com.google.gson.Gson
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
@ -11,7 +12,7 @@ import rx.Completable
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class Shikomori(private val context: Context, id: Int) : TrackService(id) {
|
||||
class Shikimori(private val context: Context, id: Int) : TrackService(id) {
|
||||
|
||||
override fun getScoreList(): List<String> {
|
||||
return IntRange(0, 10).map(Int::toString)
|
||||
@ -75,15 +76,15 @@ class Shikomori(private val context: Context, id: Int) : TrackService(id) {
|
||||
const val DEFAULT_SCORE = 0
|
||||
}
|
||||
|
||||
override val name = "Shikomori"
|
||||
override val name = "Shikimori"
|
||||
|
||||
private val gson: Gson by injectLazy()
|
||||
|
||||
private val interceptor by lazy { ShikomoriInterceptor(this, gson) }
|
||||
private val interceptor by lazy { ShikimoriInterceptor(this, gson) }
|
||||
|
||||
private val api by lazy { ShikomoriApi(client, interceptor) }
|
||||
private val api by lazy { ShikimoriApi(client, interceptor) }
|
||||
|
||||
override fun getLogo() = R.drawable.shikomori
|
||||
override fun getLogo() = R.drawable.shikimori
|
||||
|
||||
override fun getLogoColor() = Color.rgb(40, 40, 40)
|
||||
|
@ -1,4 +1,4 @@
|
||||
package eu.kanade.tachiyomi.data.track.shikomori
|
||||
package eu.kanade.tachiyomi.data.track.shikimori
|
||||
|
||||
import android.net.Uri
|
||||
import com.github.salomonbrys.kotson.array
|
||||
@ -18,7 +18,7 @@ import okhttp3.*
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class ShikomoriApi(private val client: OkHttpClient, interceptor: ShikomoriInterceptor) {
|
||||
class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInterceptor) {
|
||||
|
||||
private val gson: Gson by injectLazy()
|
||||
private val parser = JsonParser()
|
||||
@ -33,7 +33,7 @@ class ShikomoriApi(private val client: OkHttpClient, interceptor: ShikomoriInter
|
||||
"target_type" to "Manga",
|
||||
"chapters" to track.last_chapter_read,
|
||||
"score" to track.score.toInt(),
|
||||
"status" to track.toShikomoriStatus()
|
||||
"status" to track.toShikimoriStatus()
|
||||
)
|
||||
)
|
||||
val body = RequestBody.create(jsonime, payload.toString())
|
||||
@ -74,7 +74,7 @@ class ShikomoriApi(private val client: OkHttpClient, interceptor: ShikomoriInter
|
||||
}
|
||||
|
||||
private fun jsonToSearch(obj: JsonObject): TrackSearch {
|
||||
return TrackSearch.create(TrackManager.SHIKOMORI).apply {
|
||||
return TrackSearch.create(TrackManager.SHIKIMORI).apply {
|
||||
media_id = obj["id"].asInt
|
||||
title = obj["name"].asString
|
||||
total_chapters = obj["chapters"].asInt
|
||||
@ -87,14 +87,15 @@ class ShikomoriApi(private val client: OkHttpClient, interceptor: ShikomoriInter
|
||||
}
|
||||
}
|
||||
|
||||
private fun jsonToTrack(obj: JsonObject): Track {
|
||||
return Track.create(TrackManager.SHIKOMORI).apply {
|
||||
private fun jsonToTrack(obj: JsonObject, mangas: JsonObject): Track {
|
||||
return Track.create(TrackManager.SHIKIMORI).apply {
|
||||
title = mangas["name"].asString
|
||||
media_id = obj["id"].asInt
|
||||
title = ""
|
||||
total_chapters = mangas["chapters"].asInt
|
||||
last_chapter_read = obj["chapters"].asInt
|
||||
total_chapters = obj["chapters"].asInt
|
||||
score = (obj["score"].asInt).toFloat()
|
||||
status = toTrackStatus(obj["status"].asString)
|
||||
tracking_url = baseUrl + mangas["url"].asString
|
||||
}
|
||||
}
|
||||
|
||||
@ -108,21 +109,36 @@ class ShikomoriApi(private val client: OkHttpClient, interceptor: ShikomoriInter
|
||||
.url(url.toString())
|
||||
.get()
|
||||
.build()
|
||||
return authClient.newCall(request)
|
||||
|
||||
val urlMangas = Uri.parse("$apiUrl/mangas").buildUpon()
|
||||
.appendPath(track.media_id.toString())
|
||||
.build()
|
||||
val requestMangas = Request.Builder()
|
||||
.url(urlMangas.toString())
|
||||
.get()
|
||||
.build()
|
||||
return authClient.newCall(requestMangas)
|
||||
.asObservableSuccess()
|
||||
.map { netResponse ->
|
||||
val responseBody = netResponse.body()?.string().orEmpty()
|
||||
if (responseBody.isEmpty()) {
|
||||
throw Exception("Null Response")
|
||||
}
|
||||
val response = parser.parse(responseBody).array
|
||||
if (response.size() > 1) {
|
||||
throw Exception("Too much mangas in response")
|
||||
}
|
||||
val entry = response.map {
|
||||
jsonToTrack(it.obj)
|
||||
}
|
||||
entry.firstOrNull()
|
||||
parser.parse(responseBody).obj
|
||||
}.flatMap { mangas ->
|
||||
authClient.newCall(request)
|
||||
.asObservableSuccess()
|
||||
.map { netResponse ->
|
||||
val responseBody = netResponse.body()?.string().orEmpty()
|
||||
if (responseBody.isEmpty()) {
|
||||
throw Exception("Null Response")
|
||||
}
|
||||
val response = parser.parse(responseBody).array
|
||||
if (response.size() > 1) {
|
||||
throw Exception("Too much mangas in response")
|
||||
}
|
||||
val entry = response.map {
|
||||
jsonToTrack(it.obj, mangas)
|
||||
}
|
||||
entry.firstOrNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -156,10 +172,10 @@ class ShikomoriApi(private val client: OkHttpClient, interceptor: ShikomoriInter
|
||||
private const val clientId = "1aaf4cf232372708e98b5abc813d795b539c5a916dbbfe9ac61bf02a360832cc"
|
||||
private const val clientSecret = "229942c742dd4cde803125d17d64501d91c0b12e14cb1e5120184d77d67024c0"
|
||||
|
||||
private const val baseUrl = "https://shikimori.org"
|
||||
private const val apiUrl = "https://shikimori.org/api"
|
||||
private const val oauthUrl = "https://shikimori.org/oauth/token"
|
||||
private const val loginUrl = "https://shikimori.org/oauth/authorize"
|
||||
private const val baseUrl = "https://shikimori.one"
|
||||
private const val apiUrl = "https://shikimori.one/api"
|
||||
private const val oauthUrl = "https://shikimori.one/oauth/token"
|
||||
private const val loginUrl = "https://shikimori.one/oauth/authorize"
|
||||
|
||||
private const val redirectUrl = "tachiyomi://shikimori-auth"
|
||||
private const val baseMangaUrl = "$apiUrl/mangas"
|
@ -1,26 +1,26 @@
|
||||
package eu.kanade.tachiyomi.data.track.shikomori
|
||||
package eu.kanade.tachiyomi.data.track.shikimori
|
||||
|
||||
import com.google.gson.Gson
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
|
||||
class ShikomoriInterceptor(val shikomori: Shikomori, val gson: Gson) : Interceptor {
|
||||
class ShikimoriInterceptor(val shikimori: Shikimori, val gson: Gson) : Interceptor {
|
||||
|
||||
/**
|
||||
* OAuth object used for authenticated requests.
|
||||
*/
|
||||
private var oauth: OAuth? = shikomori.restoreToken()
|
||||
private var oauth: OAuth? = shikimori.restoreToken()
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val originalRequest = chain.request()
|
||||
|
||||
val currAuth = oauth ?: throw Exception("Not authenticated with Shikomori")
|
||||
val currAuth = oauth ?: throw Exception("Not authenticated with Shikimori")
|
||||
|
||||
val refreshToken = currAuth.refresh_token!!
|
||||
|
||||
// Refresh access token if expired.
|
||||
if (currAuth.isExpired()) {
|
||||
val response = chain.proceed(ShikomoriApi.refreshTokenRequest(refreshToken))
|
||||
val response = chain.proceed(ShikimoriApi.refreshTokenRequest(refreshToken))
|
||||
if (response.isSuccessful) {
|
||||
newAuth(gson.fromJson(response.body()!!.string(), OAuth::class.java))
|
||||
} else {
|
||||
@ -38,6 +38,6 @@ class ShikomoriInterceptor(val shikomori: Shikomori, val gson: Gson) : Intercept
|
||||
|
||||
fun newAuth(oauth: OAuth?) {
|
||||
this.oauth = oauth
|
||||
shikomori.saveToken(oauth)
|
||||
shikimori.saveToken(oauth)
|
||||
}
|
||||
}
|
@ -0,0 +1,24 @@
|
||||
package eu.kanade.tachiyomi.data.track.shikimori
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
|
||||
fun Track.toShikimoriStatus() = when (status) {
|
||||
Shikimori.READING -> "watching"
|
||||
Shikimori.COMPLETED -> "completed"
|
||||
Shikimori.ON_HOLD -> "on_hold"
|
||||
Shikimori.DROPPED -> "dropped"
|
||||
Shikimori.PLANNING -> "planned"
|
||||
Shikimori.REPEATING -> "rewatching"
|
||||
else -> throw NotImplementedError("Unknown status")
|
||||
}
|
||||
|
||||
fun toTrackStatus(status: String) = when (status) {
|
||||
"watching" -> Shikimori.READING
|
||||
"completed" -> Shikimori.COMPLETED
|
||||
"on_hold" -> Shikimori.ON_HOLD
|
||||
"dropped" -> Shikimori.DROPPED
|
||||
"planned" -> Shikimori.PLANNING
|
||||
"rewatching" -> Shikimori.REPEATING
|
||||
|
||||
else -> throw Exception("Unknown status")
|
||||
}
|
@ -1,24 +0,0 @@
|
||||
package eu.kanade.tachiyomi.data.track.shikomori
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
|
||||
fun Track.toShikomoriStatus() = when (status) {
|
||||
Shikomori.READING -> "watching"
|
||||
Shikomori.COMPLETED -> "completed"
|
||||
Shikomori.ON_HOLD -> "on_hold"
|
||||
Shikomori.DROPPED -> "dropped"
|
||||
Shikomori.PLANNING -> "planned"
|
||||
Shikomori.REPEATING -> "rewatching"
|
||||
else -> throw NotImplementedError("Unknown status")
|
||||
}
|
||||
|
||||
fun toTrackStatus(status: String) = when (status) {
|
||||
"watching" -> Shikomori.READING
|
||||
"completed" -> Shikomori.COMPLETED
|
||||
"on_hold" -> Shikomori.ON_HOLD
|
||||
"dropped" -> Shikomori.DROPPED
|
||||
"planned" -> Shikomori.PLANNING
|
||||
"rewatching" -> Shikomori.REPEATING
|
||||
|
||||
else -> throw Exception("Unknown status")
|
||||
}
|
@ -17,11 +17,13 @@ import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
|
||||
import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.ui.manga.info.MangaWebViewController
|
||||
import eu.kanade.tachiyomi.util.*
|
||||
import eu.kanade.tachiyomi.widget.AutofitRecyclerView
|
||||
import kotlinx.android.synthetic.main.catalogue_controller.*
|
||||
@ -259,15 +261,38 @@ open class BrowseCatalogueController(bundle: Bundle) :
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
super.onPrepareOptionsMenu(menu)
|
||||
|
||||
val isHttpSource = presenter.source is HttpSource
|
||||
menu.findItem(R.id.action_open_in_browser).isVisible = isHttpSource
|
||||
menu.findItem(R.id.action_open_in_web_view).isVisible = isHttpSource
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.action_display_mode -> swapDisplayMode()
|
||||
R.id.action_set_filter -> navView?.let { activity?.drawer?.openDrawer(Gravity.END) }
|
||||
R.id.action_open_in_browser -> openInBrowser()
|
||||
R.id.action_open_in_web_view -> openInWebView()
|
||||
else -> return super.onOptionsItemSelected(item)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun openInBrowser() {
|
||||
val source = presenter.source as? HttpSource ?: return
|
||||
|
||||
activity?.openInBrowser(source.baseUrl)
|
||||
}
|
||||
|
||||
private fun openInWebView() {
|
||||
val source = presenter.source as? HttpSource ?: return
|
||||
|
||||
router.pushController(MangaWebViewController(source.id, source.baseUrl)
|
||||
.withFadeTransaction())
|
||||
}
|
||||
|
||||
/**
|
||||
* Restarts the request with a new query.
|
||||
*
|
||||
|
@ -239,7 +239,7 @@ open class CatalogueSearchPresenter(
|
||||
* @param sManga the manga from the source.
|
||||
* @return a manga from the database.
|
||||
*/
|
||||
private fun networkToLocalManga(sManga: SManga, sourceId: Long): Manga {
|
||||
protected open fun networkToLocalManga(sManga: SManga, sourceId: Long): Manga {
|
||||
var localManga = db.getManga(sManga.url, sourceId).executeAsBlocking()
|
||||
if (localManga == null) {
|
||||
val newManga = Manga.create(sManga.url, sManga.title, sourceId)
|
||||
|
@ -185,7 +185,7 @@ class LibraryPresenter(
|
||||
|
||||
val sortFn: (LibraryItem, LibraryItem) -> Int = { i1, i2 ->
|
||||
when (sortingMode) {
|
||||
LibrarySort.ALPHA -> i1.manga.title.compareTo(i2.manga.title)
|
||||
LibrarySort.ALPHA -> i1.manga.title.compareTo(i2.manga.title, true)
|
||||
LibrarySort.LAST_READ -> {
|
||||
// Get index of manga, set equal to list if size unknown.
|
||||
val manga1LastRead = lastReadManga[i1.manga.id!!] ?: lastReadManga.size
|
||||
|
@ -24,6 +24,7 @@ import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.ui.recent_updates.RecentChaptersController
|
||||
import eu.kanade.tachiyomi.ui.recently_read.RecentlyReadController
|
||||
import eu.kanade.tachiyomi.ui.setting.SettingsMainController
|
||||
import eu.kanade.tachiyomi.util.openInBrowser
|
||||
import kotlinx.android.synthetic.main.main_activity.*
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
@ -91,6 +92,9 @@ class MainActivity : BaseActivity() {
|
||||
R.id.nav_drawer_settings -> {
|
||||
router.pushController(SettingsMainController().withFadeTransaction())
|
||||
}
|
||||
R.id.nav_drawer_help -> {
|
||||
openInBrowser(URL_HELP)
|
||||
}
|
||||
}
|
||||
}
|
||||
drawer.closeDrawer(GravityCompat.START)
|
||||
@ -271,6 +275,8 @@ class MainActivity : BaseActivity() {
|
||||
const val INTENT_SEARCH = "eu.kanade.tachiyomi.SEARCH"
|
||||
const val INTENT_SEARCH_QUERY = "query"
|
||||
const val INTENT_SEARCH_FILTER = "filter"
|
||||
|
||||
private const val URL_HELP = "https://github.com/inorichi/tachiyomi/wiki"
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -42,6 +42,7 @@ import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.util.getResourceColor
|
||||
import eu.kanade.tachiyomi.util.openInBrowser
|
||||
import eu.kanade.tachiyomi.util.snack
|
||||
import eu.kanade.tachiyomi.util.toast
|
||||
import eu.kanade.tachiyomi.util.truncateCenter
|
||||
@ -87,6 +88,9 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
|
||||
// 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() }
|
||||
|
||||
@ -287,15 +291,7 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
|
||||
val context = view?.context ?: return
|
||||
val source = presenter.source as? HttpSource ?: return
|
||||
|
||||
try {
|
||||
val url = Uri.parse(source.mangaDetailsRequest(presenter.manga).url().toString())
|
||||
val intent = CustomTabsIntent.Builder()
|
||||
.setToolbarColor(context.getResourceColor(R.attr.colorPrimary))
|
||||
.build()
|
||||
intent.launchUrl(activity, url)
|
||||
} catch (e: Exception) {
|
||||
context.toast(e.message)
|
||||
}
|
||||
context.openInBrowser(source.mangaDetailsRequest(presenter.manga).url().toString())
|
||||
}
|
||||
|
||||
private fun openInWebView() {
|
||||
@ -407,6 +403,30 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the fab is long clicked.
|
||||
*/
|
||||
private fun onFabLongClick() {
|
||||
val manga = presenter.manga
|
||||
if (!manga.favorite) {
|
||||
toggleFavorite()
|
||||
activity?.toast(activity?.getString(R.string.manga_added_library))
|
||||
}
|
||||
val categories = presenter.getCategories()
|
||||
if (categories.size <= 1) {
|
||||
// default or the one from the user then just add to favorite.
|
||||
presenter.moveMangaToCategory(manga, categories.firstOrNull())
|
||||
} 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)
|
||||
|
@ -146,6 +146,9 @@ class MigrationPresenter(
|
||||
}
|
||||
manga.favorite = true
|
||||
db.updateMangaFavorite(manga).executeAsBlocking()
|
||||
|
||||
// SearchPresenter#networkToLocalManga may have updated the manga title, so ensure db gets updated title
|
||||
db.updateMangaTitle(manga).executeAsBlocking()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.migration
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.source.CatalogueSource
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchCardItem
|
||||
import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchItem
|
||||
import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchPresenter
|
||||
@ -21,4 +22,11 @@ class SearchPresenter(
|
||||
//Set the catalogue search item as highlighted if the source matches that of the selected manga
|
||||
return CatalogueSearchItem(source, results, source.id == manga.source)
|
||||
}
|
||||
|
||||
override fun networkToLocalManga(sManga: SManga, sourceId: Long): Manga {
|
||||
val localManga = super.networkToLocalManga(sManga, sourceId)
|
||||
// For migration, displayed title should always match source rather than local DB
|
||||
localManga.title = sManga.title
|
||||
return localManga
|
||||
}
|
||||
}
|
||||
|
@ -574,6 +574,9 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
||||
|
||||
subscriptions += preferences.colorFilter().asObservable()
|
||||
.subscribe { setColorFilter(it) }
|
||||
|
||||
subscriptions += preferences.colorFilterMode().asObservable()
|
||||
.subscribe { setColorFilter(preferences.colorFilter().getOrDefault()) }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -722,7 +725,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
||||
*/
|
||||
private fun setColorFilterValue(value: Int) {
|
||||
color_overlay.visibility = View.VISIBLE
|
||||
color_overlay.setBackgroundColor(value)
|
||||
color_overlay.setFilterColor(value, preferences.colorFilterMode().getOrDefault())
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.util.plusAssign
|
||||
import eu.kanade.tachiyomi.widget.IgnoreFirstSpinnerListener
|
||||
import eu.kanade.tachiyomi.widget.SimpleSeekBarListener
|
||||
import kotlinx.android.synthetic.main.reader_color_filter.*
|
||||
import kotlinx.android.synthetic.main.reader_color_filter_sheet.*
|
||||
@ -54,6 +55,9 @@ class ReaderColorFilterSheet(activity: ReaderActivity) : BottomSheetDialog(activ
|
||||
subscriptions += preferences.colorFilter().asObservable()
|
||||
.subscribe { setColorFilter(it, view) }
|
||||
|
||||
subscriptions += preferences.colorFilterMode().asObservable()
|
||||
.subscribe { setColorFilter(preferences.colorFilter().getOrDefault(), view) }
|
||||
|
||||
subscriptions += preferences.customBrightness().asObservable()
|
||||
.subscribe { setCustomBrightness(it, view) }
|
||||
|
||||
@ -84,6 +88,11 @@ class ReaderColorFilterSheet(activity: ReaderActivity) : BottomSheetDialog(activ
|
||||
preferences.customBrightness().set(isChecked)
|
||||
}
|
||||
|
||||
color_filter_mode.onItemSelectedListener = IgnoreFirstSpinnerListener { position ->
|
||||
preferences.colorFilterMode().set(position)
|
||||
}
|
||||
color_filter_mode.setSelection(preferences.colorFilterMode().getOrDefault(), false)
|
||||
|
||||
seekbar_color_filter_alpha.setOnSeekBarChangeListener(object : SimpleSeekBarListener() {
|
||||
override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) {
|
||||
if (fromUser) {
|
||||
@ -248,7 +257,7 @@ class ReaderColorFilterSheet(activity: ReaderActivity) : BottomSheetDialog(activ
|
||||
*/
|
||||
private fun setColorFilterValue(@ColorInt color: Int, view: View) = with(view) {
|
||||
color_overlay.visibility = View.VISIBLE
|
||||
color_overlay.setBackgroundColor(color)
|
||||
color_overlay.setFilterColor(color, preferences.colorFilterMode().getOrDefault())
|
||||
setValues(color, view)
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,32 @@
|
||||
package eu.kanade.tachiyomi.ui.reader
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.*
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
|
||||
class ReaderColorFilterView(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null
|
||||
) : View(context, attrs) {
|
||||
|
||||
private val colorFilterPaint: Paint = Paint()
|
||||
|
||||
fun setFilterColor(color: Int, filterMode: Int) {
|
||||
colorFilterPaint.setColor(color)
|
||||
colorFilterPaint.xfermode = PorterDuffXfermode(when (filterMode) {
|
||||
1 -> PorterDuff.Mode.MULTIPLY
|
||||
2 -> PorterDuff.Mode.SCREEN
|
||||
3 -> PorterDuff.Mode.OVERLAY
|
||||
4 -> PorterDuff.Mode.LIGHTEN
|
||||
5 -> PorterDuff.Mode.DARKEN
|
||||
else -> PorterDuff.Mode.SRC_OVER
|
||||
})
|
||||
invalidate()
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
canvas.drawPaint(colorFilterPaint)
|
||||
}
|
||||
}
|
@ -147,10 +147,9 @@ class ReaderPresenter(
|
||||
|
||||
/**
|
||||
* Called when the user pressed the back button and is going to leave the reader. Used to
|
||||
* update tracking services and trigger deletion of the downloaded chapters.
|
||||
* trigger deletion of the downloaded chapters.
|
||||
*/
|
||||
fun onBackPressed() {
|
||||
updateTrackLastChapterRead()
|
||||
deletePendingChapters()
|
||||
}
|
||||
|
||||
@ -308,7 +307,7 @@ class ReaderPresenter(
|
||||
|
||||
/**
|
||||
* Called every time a page changes on the reader. Used to mark the flag of chapters being
|
||||
* read, enqueue downloaded chapter deletion, and updating the active chapter if this
|
||||
* read, update tracking services, enqueue downloaded chapter deletion, and updating the active chapter if this
|
||||
* [page]'s chapter is different from the currently active.
|
||||
*/
|
||||
fun onPageSelected(page: ReaderPage) {
|
||||
@ -320,6 +319,7 @@ class ReaderPresenter(
|
||||
selectedChapter.chapter.last_page_read = page.index
|
||||
if (selectedChapter.pages?.lastIndex == page.index) {
|
||||
selectedChapter.chapter.read = true
|
||||
updateTrackLastChapterRead()
|
||||
enqueueDeleteReadChapters(selectedChapter)
|
||||
}
|
||||
|
||||
@ -434,7 +434,8 @@ class ReaderPresenter(
|
||||
|
||||
// Build destination file.
|
||||
val filename = DiskUtil.buildValidFilename(
|
||||
"${manga.title} - ${chapter.name}") + " - ${page.number}.${type.extension}"
|
||||
"${manga.title} - ${chapter.name}".take(225)
|
||||
) + " - ${page.number}.${type.extension}"
|
||||
|
||||
val destFile = File(directory, filename)
|
||||
stream().use { input ->
|
||||
|
@ -8,7 +8,7 @@ import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.data.track.anilist.AnilistApi
|
||||
import eu.kanade.tachiyomi.data.track.shikomori.ShikomoriApi
|
||||
import eu.kanade.tachiyomi.data.track.shikimori.ShikimoriApi
|
||||
import eu.kanade.tachiyomi.util.getResourceColor
|
||||
import eu.kanade.tachiyomi.widget.preference.LoginPreference
|
||||
import eu.kanade.tachiyomi.widget.preference.TrackLoginDialog
|
||||
@ -54,13 +54,13 @@ class SettingsTrackingController : SettingsController(),
|
||||
dialog.showDialog(router)
|
||||
}
|
||||
}
|
||||
trackPreference(trackManager.shikomori) {
|
||||
trackPreference(trackManager.shikimori) {
|
||||
onClick {
|
||||
val tabsIntent = CustomTabsIntent.Builder()
|
||||
.setToolbarColor(context.getResourceColor(R.attr.colorPrimary))
|
||||
.build()
|
||||
tabsIntent.intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
|
||||
tabsIntent.launchUrl(activity, ShikomoriApi.authUrl())
|
||||
tabsIntent.launchUrl(activity, ShikimoriApi.authUrl())
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -80,7 +80,7 @@ class SettingsTrackingController : SettingsController(),
|
||||
super.onActivityResumed(activity)
|
||||
// Manually refresh anilist holder
|
||||
updatePreference(trackManager.aniList.id)
|
||||
updatePreference(trackManager.shikomori.id)
|
||||
updatePreference(trackManager.shikimori.id)
|
||||
}
|
||||
|
||||
private fun updatePreference(id: Int) {
|
||||
|
@ -13,7 +13,7 @@ import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
class ShikomoriLoginActivity : AppCompatActivity() {
|
||||
class ShikimoriLoginActivity : AppCompatActivity() {
|
||||
|
||||
private val trackManager: TrackManager by injectLazy()
|
||||
|
||||
@ -25,7 +25,7 @@ class ShikomoriLoginActivity : AppCompatActivity() {
|
||||
|
||||
val code = intent.data?.getQueryParameter("code")
|
||||
if (code != null) {
|
||||
trackManager.shikomori.login(code)
|
||||
trackManager.shikimori.login(code)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({
|
||||
@ -34,7 +34,7 @@ class ShikomoriLoginActivity : AppCompatActivity() {
|
||||
returnToSettings()
|
||||
})
|
||||
} else {
|
||||
trackManager.shikomori.logout()
|
||||
trackManager.shikimori.logout()
|
||||
returnToSettings()
|
||||
}
|
||||
}
|
||||
|
@ -10,14 +10,17 @@ import android.content.IntentFilter
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Resources
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.Uri
|
||||
import android.os.PowerManager
|
||||
import android.support.annotation.AttrRes
|
||||
import android.support.annotation.StringRes
|
||||
import android.support.customtabs.CustomTabsIntent
|
||||
import android.support.v4.app.NotificationCompat
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.support.v4.content.LocalBroadcastManager
|
||||
import android.widget.Toast
|
||||
import com.nononsenseapps.filepicker.FilePickerActivity
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.widget.CustomLayoutPickerActivity
|
||||
|
||||
/**
|
||||
@ -163,3 +166,18 @@ fun Context.isServiceRunning(serviceClass: Class<*>): Boolean {
|
||||
return manager.getRunningServices(Integer.MAX_VALUE)
|
||||
.any { className == it.service.className }
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a URL in a custom tab.
|
||||
*/
|
||||
fun Context.openInBrowser(url: String) {
|
||||
try {
|
||||
val url = Uri.parse(url)
|
||||
val intent = CustomTabsIntent.Builder()
|
||||
.setToolbarColor(getResourceColor(R.attr.colorPrimary))
|
||||
.build()
|
||||
intent.launchUrl(this, url)
|
||||
} catch (e: Exception) {
|
||||
toast(e.message)
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 8.6 KiB |
@ -29,7 +29,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone" />
|
||||
|
||||
<View
|
||||
<eu.kanade.tachiyomi.ui.reader.ReaderColorFilterView
|
||||
android:id="@+id/color_overlay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
@ -111,7 +111,7 @@
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<View
|
||||
<eu.kanade.tachiyomi.ui.reader.ReaderColorFilterView
|
||||
android:id="@+id/color_overlay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
@ -6,6 +6,12 @@
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:padding="16dp">
|
||||
|
||||
<android.support.v4.widget.Space
|
||||
android:id="@+id/spinner_end"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintLeft_toRightOf="parent" />
|
||||
|
||||
<!-- Color filter -->
|
||||
|
||||
<android.support.v7.widget.SwitchCompat
|
||||
@ -157,6 +163,27 @@
|
||||
app:layout_constraintBottom_toBottomOf="@id/seekbar_color_filter_alpha"
|
||||
app:layout_constraintRight_toRightOf="parent"/>
|
||||
|
||||
<!-- Filter mode -->
|
||||
|
||||
<TextView
|
||||
android:id="@+id/color_filter_mode_text"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/pref_color_filter_mode"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toLeftOf="@id/color_filter_mode"
|
||||
app:layout_constraintBaseline_toBaselineOf="@id/color_filter_mode"/>
|
||||
|
||||
<android.support.v7.widget.AppCompatSpinner
|
||||
android:id="@+id/color_filter_mode"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:entries="@array/color_filter_modes"
|
||||
app:layout_constraintTop_toBottomOf="@id/seekbar_color_filter_alpha"
|
||||
app:layout_constraintLeft_toRightOf="@id/verticalcenter"
|
||||
app:layout_constraintRight_toRightOf="@id/spinner_end" />
|
||||
|
||||
<!-- Brightness -->
|
||||
|
||||
<android.support.v7.widget.SwitchCompat
|
||||
@ -165,7 +192,7 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:text="@string/pref_custom_brightness"
|
||||
app:layout_constraintTop_toBottomOf="@id/seekbar_color_filter_alpha"/>
|
||||
app:layout_constraintTop_toBottomOf="@id/color_filter_mode_text"/>
|
||||
|
||||
<!-- Brightness value -->
|
||||
|
||||
@ -202,4 +229,11 @@
|
||||
app:layout_constraintBottom_toBottomOf="@id/brightness_seekbar"
|
||||
app:layout_constraintRight_toRightOf="parent"/>
|
||||
|
||||
<android.support.constraint.Guideline
|
||||
android:id="@+id/verticalcenter"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintGuide_percent="0.5" />
|
||||
|
||||
</android.support.constraint.ConstraintLayout>
|
||||
|
@ -21,7 +21,7 @@
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone" />
|
||||
|
||||
<View
|
||||
<eu.kanade.tachiyomi.ui.reader.ReaderColorFilterView
|
||||
android:id="@+id/color_overlay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
@ -19,4 +19,15 @@
|
||||
android:id="@+id/action_display_mode"
|
||||
android:title="@string/action_display_mode"
|
||||
app:showAsAction="ifRoom"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/action_open_in_browser"
|
||||
android:title="@string/action_open_in_browser"
|
||||
app:showAsAction="never"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/action_open_in_web_view"
|
||||
android:title="@string/action_open_in_web_view"
|
||||
app:showAsAction="never"/>
|
||||
|
||||
</menu>
|
||||
|
@ -36,5 +36,10 @@
|
||||
android:icon="@drawable/ic_settings_black_24dp"
|
||||
android:title="@string/label_settings"
|
||||
android:checkable="false" />
|
||||
<item
|
||||
android:id="@+id/nav_drawer_help"
|
||||
android:icon="@drawable/ic_help_black_24dp"
|
||||
android:title="@string/label_help"
|
||||
android:checkable="false" />
|
||||
</group>
|
||||
</menu>
|
||||
|
15
app/src/main/res/values-v28/arrays.xml
Normal file
15
app/src/main/res/values-v28/arrays.xml
Normal file
@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<string-array name="color_filter_modes">
|
||||
<item>@string/filter_mode_default</item>
|
||||
<item>@string/filter_mode_multiply</item>
|
||||
<item>@string/filter_mode_screen</item>
|
||||
|
||||
<!-- Attributes specific for SDK 28 and up -->
|
||||
<item>@string/filter_mode_overlay</item>
|
||||
<item>@string/filter_mode_lighten</item>
|
||||
<item>@string/filter_mode_darken</item>
|
||||
</string-array>
|
||||
|
||||
</resources>
|
@ -103,4 +103,10 @@
|
||||
<item>2</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="color_filter_modes">
|
||||
<item>@string/filter_mode_default</item>
|
||||
<item>@string/filter_mode_multiply</item>
|
||||
<item>@string/filter_mode_screen</item>
|
||||
</string-array>
|
||||
|
||||
</resources>
|
||||
|
@ -24,6 +24,7 @@
|
||||
<string name="label_migration">Source migration</string>
|
||||
<string name="label_extensions">Extensions</string>
|
||||
<string name="label_extension_info">Extension info</string>
|
||||
<string name="label_help">Help</string>
|
||||
|
||||
|
||||
<!-- Actions -->
|
||||
@ -178,6 +179,13 @@
|
||||
<string name="pref_crop_borders">Crop borders</string>
|
||||
<string name="pref_custom_brightness">Use custom brightness</string>
|
||||
<string name="pref_custom_color_filter">Use custom color filter</string>
|
||||
<string name="pref_color_filter_mode">Color filter blend mode</string>
|
||||
<string name="filter_mode_default">Default</string>
|
||||
<string name="filter_mode_overlay">Overlay</string>
|
||||
<string name="filter_mode_multiply">Multiply</string>
|
||||
<string name="filter_mode_screen">Screen</string>
|
||||
<string name="filter_mode_lighten">Dodge / Lighten</string>
|
||||
<string name="filter_mode_darken">Burn / Darken</string>
|
||||
<string name="pref_keep_screen_on">Keep screen on</string>
|
||||
<string name="pref_skip_read_chapters">Skip chapters marked read</string>
|
||||
<string name="pref_reader_navigation">Navigation</string>
|
||||
|
Loading…
Reference in New Issue
Block a user