Merge remote-tracking branch 'upstream/master' into Automatic_Reader_Background

This commit is contained in:
Jay 2019-06-08 15:59:29 -07:00
commit b4b78e0f6b
39 changed files with 364 additions and 100 deletions

2
.github/FUNDING.yml vendored Normal file
View File

@ -0,0 +1,2 @@
github: inorichi
ko_fi: inorichi

View File

@ -11,7 +11,7 @@ Tachiyomi is a free and open source manga reader for Android.
## Features ## Features
Features include: 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 * Local reading of downloaded manga
* Configurable reader with multiple viewers, reading directions and other settings * 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 * [MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), and [Kitsu](https://kitsu.io/explore/anime) support

View File

@ -62,8 +62,8 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity <activity
android:name=".ui.setting.ShikomoriLoginActivity" android:name=".ui.setting.ShikimoriLoginActivity"
android:label="Shikomori"> android:label="Shikimori">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />

View File

@ -82,6 +82,11 @@ interface MangaQueries : DbProvider {
.withPutResolver(MangaViewerPutResolver()) .withPutResolver(MangaViewerPutResolver())
.prepare() .prepare()
fun updateMangaTitle(manga: Manga) = db.put()
.`object`(manga)
.withPutResolver(MangaTitlePutResolver())
.prepare()
fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare() fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare()
fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare() fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare()

View File

@ -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)
}
}

View File

@ -20,10 +20,10 @@ class MangaViewerPutResolver : PutResolver<Manga>() {
} }
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder() fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
.table(MangaTable.TABLE) .table(MangaTable.TABLE)
.where("${MangaTable.COL_ID} = ?") .where("${MangaTable.COL_ID} = ?")
.whereArgs(manga.id) .whereArgs(manga.id)
.build() .build()
fun mapToContentValues(manga: Manga) = ContentValues(1).apply { fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
put(MangaTable.COL_VIEWER, manga.viewer) put(MangaTable.COL_VIEWER, manga.viewer)

View File

@ -29,6 +29,8 @@ object PreferenceKeys {
const val colorFilterValue = "color_filter_value" const val colorFilterValue = "color_filter_value"
const val colorFilterMode = "color_filter_mode"
const val defaultViewer = "pref_default_viewer_key" const val defaultViewer = "pref_default_viewer_key"
const val imageScaleType = "pref_image_scale_type_key" const val imageScaleType = "pref_image_scale_type_key"

View File

@ -57,6 +57,8 @@ class PreferencesHelper(val context: Context) {
fun colorFilterValue() = rxPrefs.getInteger(Keys.colorFilterValue, 0) fun colorFilterValue() = rxPrefs.getInteger(Keys.colorFilterValue, 0)
fun colorFilterMode() = rxPrefs.getInteger(Keys.colorFilterMode, 0)
fun defaultViewer() = prefs.getInt(Keys.defaultViewer, 1) fun defaultViewer() = prefs.getInt(Keys.defaultViewer, 1)
fun imageScaleType() = rxPrefs.getInteger(Keys.imageScaleType, 1) fun imageScaleType() = rxPrefs.getInteger(Keys.imageScaleType, 1)

View File

@ -4,7 +4,7 @@ import android.content.Context
import eu.kanade.tachiyomi.data.track.anilist.Anilist import eu.kanade.tachiyomi.data.track.anilist.Anilist
import eu.kanade.tachiyomi.data.track.kitsu.Kitsu import eu.kanade.tachiyomi.data.track.kitsu.Kitsu
import eu.kanade.tachiyomi.data.track.myanimelist.Myanimelist 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) { class TrackManager(private val context: Context) {
@ -12,7 +12,7 @@ class TrackManager(private val context: Context) {
const val MYANIMELIST = 1 const val MYANIMELIST = 1
const val ANILIST = 2 const val ANILIST = 2
const val KITSU = 3 const val KITSU = 3
const val SHIKOMORI = 4 const val SHIKIMORI = 4
} }
val myAnimeList = Myanimelist(context, MYANIMELIST) val myAnimeList = Myanimelist(context, MYANIMELIST)
@ -21,9 +21,9 @@ class TrackManager(private val context: Context) {
val kitsu = Kitsu(context, KITSU) 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 } fun getService(id: Int) = services.find { it.id == id }

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.data.track.shikomori package eu.kanade.tachiyomi.data.track.shikimori
data class OAuth( data class OAuth(
val access_token: String, val access_token: String,

View File

@ -1,7 +1,8 @@
package eu.kanade.tachiyomi.data.track.shikomori package eu.kanade.tachiyomi.data.track.shikimori
import android.content.Context import android.content.Context
import android.graphics.Color import android.graphics.Color
import android.util.Log
import com.google.gson.Gson import com.google.gson.Gson
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
@ -11,7 +12,7 @@ import rx.Completable
import rx.Observable import rx.Observable
import uy.kohesive.injekt.injectLazy 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> { override fun getScoreList(): List<String> {
return IntRange(0, 10).map(Int::toString) 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 const val DEFAULT_SCORE = 0
} }
override val name = "Shikomori" override val name = "Shikimori"
private val gson: Gson by injectLazy() 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) override fun getLogoColor() = Color.rgb(40, 40, 40)

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.data.track.shikomori package eu.kanade.tachiyomi.data.track.shikimori
import android.net.Uri import android.net.Uri
import com.github.salomonbrys.kotson.array import com.github.salomonbrys.kotson.array
@ -18,7 +18,7 @@ import okhttp3.*
import rx.Observable import rx.Observable
import uy.kohesive.injekt.injectLazy 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 gson: Gson by injectLazy()
private val parser = JsonParser() private val parser = JsonParser()
@ -33,7 +33,7 @@ class ShikomoriApi(private val client: OkHttpClient, interceptor: ShikomoriInter
"target_type" to "Manga", "target_type" to "Manga",
"chapters" to track.last_chapter_read, "chapters" to track.last_chapter_read,
"score" to track.score.toInt(), "score" to track.score.toInt(),
"status" to track.toShikomoriStatus() "status" to track.toShikimoriStatus()
) )
) )
val body = RequestBody.create(jsonime, payload.toString()) 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 { private fun jsonToSearch(obj: JsonObject): TrackSearch {
return TrackSearch.create(TrackManager.SHIKOMORI).apply { return TrackSearch.create(TrackManager.SHIKIMORI).apply {
media_id = obj["id"].asInt media_id = obj["id"].asInt
title = obj["name"].asString title = obj["name"].asString
total_chapters = obj["chapters"].asInt total_chapters = obj["chapters"].asInt
@ -87,14 +87,15 @@ class ShikomoriApi(private val client: OkHttpClient, interceptor: ShikomoriInter
} }
} }
private fun jsonToTrack(obj: JsonObject): Track { private fun jsonToTrack(obj: JsonObject, mangas: JsonObject): Track {
return Track.create(TrackManager.SHIKOMORI).apply { return Track.create(TrackManager.SHIKIMORI).apply {
title = mangas["name"].asString
media_id = obj["id"].asInt media_id = obj["id"].asInt
title = "" total_chapters = mangas["chapters"].asInt
last_chapter_read = obj["chapters"].asInt last_chapter_read = obj["chapters"].asInt
total_chapters = obj["chapters"].asInt
score = (obj["score"].asInt).toFloat() score = (obj["score"].asInt).toFloat()
status = toTrackStatus(obj["status"].asString) 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()) .url(url.toString())
.get() .get()
.build() .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() .asObservableSuccess()
.map { netResponse -> .map { netResponse ->
val responseBody = netResponse.body()?.string().orEmpty() val responseBody = netResponse.body()?.string().orEmpty()
if (responseBody.isEmpty()) { parser.parse(responseBody).obj
throw Exception("Null Response") }.flatMap { mangas ->
} authClient.newCall(request)
val response = parser.parse(responseBody).array .asObservableSuccess()
if (response.size() > 1) { .map { netResponse ->
throw Exception("Too much mangas in response") val responseBody = netResponse.body()?.string().orEmpty()
} if (responseBody.isEmpty()) {
val entry = response.map { throw Exception("Null Response")
jsonToTrack(it.obj) }
} val response = parser.parse(responseBody).array
entry.firstOrNull() 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 clientId = "1aaf4cf232372708e98b5abc813d795b539c5a916dbbfe9ac61bf02a360832cc"
private const val clientSecret = "229942c742dd4cde803125d17d64501d91c0b12e14cb1e5120184d77d67024c0" private const val clientSecret = "229942c742dd4cde803125d17d64501d91c0b12e14cb1e5120184d77d67024c0"
private const val baseUrl = "https://shikimori.org" private const val baseUrl = "https://shikimori.one"
private const val apiUrl = "https://shikimori.org/api" private const val apiUrl = "https://shikimori.one/api"
private const val oauthUrl = "https://shikimori.org/oauth/token" private const val oauthUrl = "https://shikimori.one/oauth/token"
private const val loginUrl = "https://shikimori.org/oauth/authorize" private const val loginUrl = "https://shikimori.one/oauth/authorize"
private const val redirectUrl = "tachiyomi://shikimori-auth" private const val redirectUrl = "tachiyomi://shikimori-auth"
private const val baseMangaUrl = "$apiUrl/mangas" private const val baseMangaUrl = "$apiUrl/mangas"

View File

@ -1,26 +1,26 @@
package eu.kanade.tachiyomi.data.track.shikomori package eu.kanade.tachiyomi.data.track.shikimori
import com.google.gson.Gson import com.google.gson.Gson
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Response 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. * 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 { override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request() 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!! val refreshToken = currAuth.refresh_token!!
// Refresh access token if expired. // Refresh access token if expired.
if (currAuth.isExpired()) { if (currAuth.isExpired()) {
val response = chain.proceed(ShikomoriApi.refreshTokenRequest(refreshToken)) val response = chain.proceed(ShikimoriApi.refreshTokenRequest(refreshToken))
if (response.isSuccessful) { if (response.isSuccessful) {
newAuth(gson.fromJson(response.body()!!.string(), OAuth::class.java)) newAuth(gson.fromJson(response.body()!!.string(), OAuth::class.java))
} else { } else {
@ -38,6 +38,6 @@ class ShikomoriInterceptor(val shikomori: Shikomori, val gson: Gson) : Intercept
fun newAuth(oauth: OAuth?) { fun newAuth(oauth: OAuth?) {
this.oauth = oauth this.oauth = oauth
shikomori.saveToken(oauth) shikimori.saveToken(oauth)
} }
} }

View File

@ -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")
}

View File

@ -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")
}

View File

@ -17,11 +17,13 @@ import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.model.FilterList 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.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController import eu.kanade.tachiyomi.ui.base.controller.SecondaryDrawerController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.ui.manga.info.MangaWebViewController
import eu.kanade.tachiyomi.util.* import eu.kanade.tachiyomi.util.*
import eu.kanade.tachiyomi.widget.AutofitRecyclerView import eu.kanade.tachiyomi.widget.AutofitRecyclerView
import kotlinx.android.synthetic.main.catalogue_controller.* 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 { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.action_display_mode -> swapDisplayMode() R.id.action_display_mode -> swapDisplayMode()
R.id.action_set_filter -> navView?.let { activity?.drawer?.openDrawer(Gravity.END) } 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) else -> return super.onOptionsItemSelected(item)
} }
return true 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. * Restarts the request with a new query.
* *

View File

@ -239,7 +239,7 @@ open class CatalogueSearchPresenter(
* @param sManga the manga from the source. * @param sManga the manga from the source.
* @return a manga from the database. * @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() var localManga = db.getManga(sManga.url, sourceId).executeAsBlocking()
if (localManga == null) { if (localManga == null) {
val newManga = Manga.create(sManga.url, sManga.title, sourceId) val newManga = Manga.create(sManga.url, sManga.title, sourceId)

View File

@ -185,7 +185,7 @@ class LibraryPresenter(
val sortFn: (LibraryItem, LibraryItem) -> Int = { i1, i2 -> val sortFn: (LibraryItem, LibraryItem) -> Int = { i1, i2 ->
when (sortingMode) { when (sortingMode) {
LibrarySort.ALPHA -> i1.manga.title.compareTo(i2.manga.title) LibrarySort.ALPHA -> i1.manga.title.compareTo(i2.manga.title, true)
LibrarySort.LAST_READ -> { LibrarySort.LAST_READ -> {
// Get index of manga, set equal to list if size unknown. // Get index of manga, set equal to list if size unknown.
val manga1LastRead = lastReadManga[i1.manga.id!!] ?: lastReadManga.size val manga1LastRead = lastReadManga[i1.manga.id!!] ?: lastReadManga.size

View File

@ -24,6 +24,7 @@ import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.ui.recent_updates.RecentChaptersController import eu.kanade.tachiyomi.ui.recent_updates.RecentChaptersController
import eu.kanade.tachiyomi.ui.recently_read.RecentlyReadController import eu.kanade.tachiyomi.ui.recently_read.RecentlyReadController
import eu.kanade.tachiyomi.ui.setting.SettingsMainController import eu.kanade.tachiyomi.ui.setting.SettingsMainController
import eu.kanade.tachiyomi.util.openInBrowser
import kotlinx.android.synthetic.main.main_activity.* import kotlinx.android.synthetic.main.main_activity.*
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@ -91,6 +92,9 @@ class MainActivity : BaseActivity() {
R.id.nav_drawer_settings -> { R.id.nav_drawer_settings -> {
router.pushController(SettingsMainController().withFadeTransaction()) router.pushController(SettingsMainController().withFadeTransaction())
} }
R.id.nav_drawer_help -> {
openInBrowser(URL_HELP)
}
} }
} }
drawer.closeDrawer(GravityCompat.START) drawer.closeDrawer(GravityCompat.START)
@ -271,6 +275,8 @@ class MainActivity : BaseActivity() {
const val INTENT_SEARCH = "eu.kanade.tachiyomi.SEARCH" const val INTENT_SEARCH = "eu.kanade.tachiyomi.SEARCH"
const val INTENT_SEARCH_QUERY = "query" const val INTENT_SEARCH_QUERY = "query"
const val INTENT_SEARCH_FILTER = "filter" const val INTENT_SEARCH_FILTER = "filter"
private const val URL_HELP = "https://github.com/inorichi/tachiyomi/wiki"
} }
} }

View File

@ -42,6 +42,7 @@ import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
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.MangaController
import eu.kanade.tachiyomi.util.getResourceColor import eu.kanade.tachiyomi.util.getResourceColor
import eu.kanade.tachiyomi.util.openInBrowser
import eu.kanade.tachiyomi.util.snack import eu.kanade.tachiyomi.util.snack
import eu.kanade.tachiyomi.util.toast import eu.kanade.tachiyomi.util.toast
import eu.kanade.tachiyomi.util.truncateCenter import eu.kanade.tachiyomi.util.truncateCenter
@ -87,6 +88,9 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
// Set onclickListener to toggle favorite when FAB clicked. // Set onclickListener to toggle favorite when FAB clicked.
fab_favorite.clicks().subscribeUntilDestroy { onFabClick() } 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. // Set SwipeRefresh to refresh manga data.
swipe_refresh.refreshes().subscribeUntilDestroy { fetchMangaFromSource() } swipe_refresh.refreshes().subscribeUntilDestroy { fetchMangaFromSource() }
@ -287,15 +291,7 @@ class MangaInfoController : NucleusController<MangaInfoPresenter>(),
val context = view?.context ?: return val context = view?.context ?: return
val source = presenter.source as? HttpSource ?: return val source = presenter.source as? HttpSource ?: return
try { context.openInBrowser(source.mangaDetailsRequest(presenter.manga).url().toString())
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)
}
} }
private fun openInWebView() { 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>) { override fun updateCategoriesForMangas(mangas: List<Manga>, categories: List<Category>) {
val manga = mangas.firstOrNull() ?: return val manga = mangas.firstOrNull() ?: return
presenter.moveMangaToCategories(manga, categories) presenter.moveMangaToCategories(manga, categories)

View File

@ -146,6 +146,9 @@ class MigrationPresenter(
} }
manga.favorite = true manga.favorite = true
db.updateMangaFavorite(manga).executeAsBlocking() db.updateMangaFavorite(manga).executeAsBlocking()
// SearchPresenter#networkToLocalManga may have updated the manga title, so ensure db gets updated title
db.updateMangaTitle(manga).executeAsBlocking()
} }
} }
} }

View File

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.migration
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.source.CatalogueSource 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.CatalogueSearchCardItem
import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchItem import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchItem
import eu.kanade.tachiyomi.ui.catalogue.global_search.CatalogueSearchPresenter 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 //Set the catalogue search item as highlighted if the source matches that of the selected manga
return CatalogueSearchItem(source, results, source.id == manga.source) 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
}
} }

View File

@ -574,6 +574,9 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
subscriptions += preferences.colorFilter().asObservable() subscriptions += preferences.colorFilter().asObservable()
.subscribe { setColorFilter(it) } .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) { private fun setColorFilterValue(value: Int) {
color_overlay.visibility = View.VISIBLE color_overlay.visibility = View.VISIBLE
color_overlay.setBackgroundColor(value) color_overlay.setFilterColor(value, preferences.colorFilterMode().getOrDefault())
} }
} }

View File

@ -11,6 +11,7 @@ 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.util.plusAssign import eu.kanade.tachiyomi.util.plusAssign
import eu.kanade.tachiyomi.widget.IgnoreFirstSpinnerListener
import eu.kanade.tachiyomi.widget.SimpleSeekBarListener import eu.kanade.tachiyomi.widget.SimpleSeekBarListener
import kotlinx.android.synthetic.main.reader_color_filter.* import kotlinx.android.synthetic.main.reader_color_filter.*
import kotlinx.android.synthetic.main.reader_color_filter_sheet.* import kotlinx.android.synthetic.main.reader_color_filter_sheet.*
@ -54,6 +55,9 @@ class ReaderColorFilterSheet(activity: ReaderActivity) : BottomSheetDialog(activ
subscriptions += preferences.colorFilter().asObservable() subscriptions += preferences.colorFilter().asObservable()
.subscribe { setColorFilter(it, view) } .subscribe { setColorFilter(it, view) }
subscriptions += preferences.colorFilterMode().asObservable()
.subscribe { setColorFilter(preferences.colorFilter().getOrDefault(), view) }
subscriptions += preferences.customBrightness().asObservable() subscriptions += preferences.customBrightness().asObservable()
.subscribe { setCustomBrightness(it, view) } .subscribe { setCustomBrightness(it, view) }
@ -84,6 +88,11 @@ class ReaderColorFilterSheet(activity: ReaderActivity) : BottomSheetDialog(activ
preferences.customBrightness().set(isChecked) 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() { seekbar_color_filter_alpha.setOnSeekBarChangeListener(object : SimpleSeekBarListener() {
override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) { override fun onProgressChanged(seekBar: SeekBar, value: Int, fromUser: Boolean) {
if (fromUser) { if (fromUser) {
@ -248,7 +257,7 @@ class ReaderColorFilterSheet(activity: ReaderActivity) : BottomSheetDialog(activ
*/ */
private fun setColorFilterValue(@ColorInt color: Int, view: View) = with(view) { private fun setColorFilterValue(@ColorInt color: Int, view: View) = with(view) {
color_overlay.visibility = View.VISIBLE color_overlay.visibility = View.VISIBLE
color_overlay.setBackgroundColor(color) color_overlay.setFilterColor(color, preferences.colorFilterMode().getOrDefault())
setValues(color, view) setValues(color, view)
} }

View File

@ -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)
}
}

View File

@ -147,10 +147,9 @@ class ReaderPresenter(
/** /**
* Called when the user pressed the back button and is going to leave the reader. Used to * 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() { fun onBackPressed() {
updateTrackLastChapterRead()
deletePendingChapters() deletePendingChapters()
} }
@ -308,7 +307,7 @@ class ReaderPresenter(
/** /**
* Called every time a page changes on the reader. Used to mark the flag of chapters being * 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. * [page]'s chapter is different from the currently active.
*/ */
fun onPageSelected(page: ReaderPage) { fun onPageSelected(page: ReaderPage) {
@ -320,6 +319,7 @@ class ReaderPresenter(
selectedChapter.chapter.last_page_read = page.index selectedChapter.chapter.last_page_read = page.index
if (selectedChapter.pages?.lastIndex == page.index) { if (selectedChapter.pages?.lastIndex == page.index) {
selectedChapter.chapter.read = true selectedChapter.chapter.read = true
updateTrackLastChapterRead()
enqueueDeleteReadChapters(selectedChapter) enqueueDeleteReadChapters(selectedChapter)
} }
@ -434,7 +434,8 @@ class ReaderPresenter(
// Build destination file. // Build destination file.
val filename = DiskUtil.buildValidFilename( 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) val destFile = File(directory, filename)
stream().use { input -> stream().use { input ->

View File

@ -8,7 +8,7 @@ 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.data.track.anilist.AnilistApi 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.util.getResourceColor
import eu.kanade.tachiyomi.widget.preference.LoginPreference import eu.kanade.tachiyomi.widget.preference.LoginPreference
import eu.kanade.tachiyomi.widget.preference.TrackLoginDialog import eu.kanade.tachiyomi.widget.preference.TrackLoginDialog
@ -54,13 +54,13 @@ class SettingsTrackingController : SettingsController(),
dialog.showDialog(router) dialog.showDialog(router)
} }
} }
trackPreference(trackManager.shikomori) { trackPreference(trackManager.shikimori) {
onClick { onClick {
val tabsIntent = CustomTabsIntent.Builder() val tabsIntent = CustomTabsIntent.Builder()
.setToolbarColor(context.getResourceColor(R.attr.colorPrimary)) .setToolbarColor(context.getResourceColor(R.attr.colorPrimary))
.build() .build()
tabsIntent.intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY) 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) super.onActivityResumed(activity)
// Manually refresh anilist holder // Manually refresh anilist holder
updatePreference(trackManager.aniList.id) updatePreference(trackManager.aniList.id)
updatePreference(trackManager.shikomori.id) updatePreference(trackManager.shikimori.id)
} }
private fun updatePreference(id: Int) { private fun updatePreference(id: Int) {

View File

@ -13,7 +13,7 @@ import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
class ShikomoriLoginActivity : AppCompatActivity() { class ShikimoriLoginActivity : AppCompatActivity() {
private val trackManager: TrackManager by injectLazy() private val trackManager: TrackManager by injectLazy()
@ -25,7 +25,7 @@ class ShikomoriLoginActivity : AppCompatActivity() {
val code = intent.data?.getQueryParameter("code") val code = intent.data?.getQueryParameter("code")
if (code != null) { if (code != null) {
trackManager.shikomori.login(code) trackManager.shikimori.login(code)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe({ .subscribe({
@ -34,7 +34,7 @@ class ShikomoriLoginActivity : AppCompatActivity() {
returnToSettings() returnToSettings()
}) })
} else { } else {
trackManager.shikomori.logout() trackManager.shikimori.logout()
returnToSettings() returnToSettings()
} }
} }

View File

@ -10,14 +10,17 @@ import android.content.IntentFilter
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.res.Resources import android.content.res.Resources
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.Uri
import android.os.PowerManager import android.os.PowerManager
import android.support.annotation.AttrRes import android.support.annotation.AttrRes
import android.support.annotation.StringRes import android.support.annotation.StringRes
import android.support.customtabs.CustomTabsIntent
import android.support.v4.app.NotificationCompat import android.support.v4.app.NotificationCompat
import android.support.v4.content.ContextCompat import android.support.v4.content.ContextCompat
import android.support.v4.content.LocalBroadcastManager import android.support.v4.content.LocalBroadcastManager
import android.widget.Toast import android.widget.Toast
import com.nononsenseapps.filepicker.FilePickerActivity import com.nononsenseapps.filepicker.FilePickerActivity
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.widget.CustomLayoutPickerActivity import eu.kanade.tachiyomi.widget.CustomLayoutPickerActivity
/** /**
@ -163,3 +166,18 @@ fun Context.isServiceRunning(serviceClass: Class<*>): Boolean {
return manager.getRunningServices(Integer.MAX_VALUE) return manager.getRunningServices(Integer.MAX_VALUE)
.any { className == it.service.className } .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)
}
}

View File

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 8.6 KiB

View File

@ -29,7 +29,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:visibility="gone" /> android:visibility="gone" />
<View <eu.kanade.tachiyomi.ui.reader.ReaderColorFilterView
android:id="@+id/color_overlay" android:id="@+id/color_overlay"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"

View File

@ -111,7 +111,7 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:visibility="gone"/> android:visibility="gone"/>
<View <eu.kanade.tachiyomi.ui.reader.ReaderColorFilterView
android:id="@+id/color_overlay" android:id="@+id/color_overlay"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"

View File

@ -6,6 +6,12 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:padding="16dp"> 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 --> <!-- Color filter -->
<android.support.v7.widget.SwitchCompat <android.support.v7.widget.SwitchCompat
@ -157,6 +163,27 @@
app:layout_constraintBottom_toBottomOf="@id/seekbar_color_filter_alpha" app:layout_constraintBottom_toBottomOf="@id/seekbar_color_filter_alpha"
app:layout_constraintRight_toRightOf="parent"/> 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 --> <!-- Brightness -->
<android.support.v7.widget.SwitchCompat <android.support.v7.widget.SwitchCompat
@ -165,7 +192,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:text="@string/pref_custom_brightness" 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 --> <!-- Brightness value -->
@ -202,4 +229,11 @@
app:layout_constraintBottom_toBottomOf="@id/brightness_seekbar" app:layout_constraintBottom_toBottomOf="@id/brightness_seekbar"
app:layout_constraintRight_toRightOf="parent"/> 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> </android.support.constraint.ConstraintLayout>

View File

@ -21,7 +21,7 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:visibility="gone" /> android:visibility="gone" />
<View <eu.kanade.tachiyomi.ui.reader.ReaderColorFilterView
android:id="@+id/color_overlay" android:id="@+id/color_overlay"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"

View File

@ -19,4 +19,15 @@
android:id="@+id/action_display_mode" android:id="@+id/action_display_mode"
android:title="@string/action_display_mode" android:title="@string/action_display_mode"
app:showAsAction="ifRoom"/> 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> </menu>

View File

@ -36,5 +36,10 @@
android:icon="@drawable/ic_settings_black_24dp" android:icon="@drawable/ic_settings_black_24dp"
android:title="@string/label_settings" android:title="@string/label_settings"
android:checkable="false" /> 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> </group>
</menu> </menu>

View 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>

View File

@ -103,4 +103,10 @@
<item>2</item> <item>2</item>
</string-array> </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> </resources>

View File

@ -24,6 +24,7 @@
<string name="label_migration">Source migration</string> <string name="label_migration">Source migration</string>
<string name="label_extensions">Extensions</string> <string name="label_extensions">Extensions</string>
<string name="label_extension_info">Extension info</string> <string name="label_extension_info">Extension info</string>
<string name="label_help">Help</string>
<!-- Actions --> <!-- Actions -->
@ -178,6 +179,13 @@
<string name="pref_crop_borders">Crop borders</string> <string name="pref_crop_borders">Crop borders</string>
<string name="pref_custom_brightness">Use custom brightness</string> <string name="pref_custom_brightness">Use custom brightness</string>
<string name="pref_custom_color_filter">Use custom color filter</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_keep_screen_on">Keep screen on</string>
<string name="pref_skip_read_chapters">Skip chapters marked read</string> <string name="pref_skip_read_chapters">Skip chapters marked read</string>
<string name="pref_reader_navigation">Navigation</string> <string name="pref_reader_navigation">Navigation</string>