mirror of
https://github.com/mihonapp/mihon.git
synced 2025-06-26 19:17:51 +02:00
Compare commits
24 Commits
Author | SHA1 | Date | |
---|---|---|---|
fd76255cf6 | |||
d180631877 | |||
1977e21363 | |||
e1a3ee1b81 | |||
cc43d9daed | |||
79705df499 | |||
36bbb906c1 | |||
816cc17ed3 | |||
97e3b5d2ab | |||
79ab9d80f2 | |||
32511149d1 | |||
cc9fd53abb | |||
4061c7450b | |||
9ad535bde6 | |||
b067096fc7 | |||
2dd58e5f7d | |||
7c42ab885b | |||
26b283d44d | |||
8c1b07c4ba | |||
f98e0858a7 | |||
8b60d5bfcb | |||
30b4c6e755 | |||
3d2a98451b | |||
aba528b227 |
@ -38,8 +38,8 @@ android {
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 25
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
versionCode 16
|
||||
versionName "0.4.0"
|
||||
versionCode 17
|
||||
versionName "0.4.1"
|
||||
|
||||
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
|
||||
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
|
||||
|
@ -2,10 +2,12 @@ package eu.kanade.tachiyomi
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.support.multidex.MultiDex
|
||||
import com.evernote.android.job.JobManager
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||
import eu.kanade.tachiyomi.data.updater.UpdateCheckerJob
|
||||
import eu.kanade.tachiyomi.util.LocaleHelper
|
||||
import org.acra.ACRA
|
||||
import org.acra.annotation.ReportsCrashes
|
||||
import timber.log.Timber
|
||||
@ -31,6 +33,8 @@ open class App : Application() {
|
||||
|
||||
setupAcra()
|
||||
setupJobManager()
|
||||
|
||||
LocaleHelper.updateCfg(this, baseContext.resources.configuration)
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context) {
|
||||
@ -40,6 +44,11 @@ open class App : Application() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
LocaleHelper.updateCfg(this, newConfig)
|
||||
}
|
||||
|
||||
protected open fun setupAcra() {
|
||||
ACRA.init(this)
|
||||
}
|
||||
|
@ -6,4 +6,5 @@ object Constants {
|
||||
const val NOTIFICATION_DOWNLOAD_CHAPTER_ID = 3
|
||||
const val NOTIFICATION_DOWNLOAD_CHAPTER_ERROR_ID = 4
|
||||
const val NOTIFICATION_DOWNLOAD_IMAGE_ID = 5
|
||||
|
||||
}
|
||||
|
@ -40,7 +40,6 @@ interface HistoryQueries : DbProvider {
|
||||
.build())
|
||||
.prepare()
|
||||
|
||||
|
||||
/**
|
||||
* Updates the history last read.
|
||||
* Inserts history object if not yet in database
|
||||
|
@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.data.database.DbProvider
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||
@ -29,7 +30,7 @@ interface MangaQueries : DbProvider {
|
||||
.withGetResolver(LibraryMangaGetResolver.INSTANCE)
|
||||
.prepare()
|
||||
|
||||
open fun getFavoriteMangas() = db.get()
|
||||
fun getFavoriteMangas() = db.get()
|
||||
.listOfObjects(Manga::class.java)
|
||||
.withQuery(Query.builder()
|
||||
.table(MangaTable.TABLE)
|
||||
@ -66,6 +67,11 @@ interface MangaQueries : DbProvider {
|
||||
.withPutResolver(MangaFlagsPutResolver())
|
||||
.prepare()
|
||||
|
||||
fun updateLastUpdated(manga: Manga) = db.put()
|
||||
.`object`(manga)
|
||||
.withPutResolver(MangaLastUpdatedPutResolver())
|
||||
.prepare()
|
||||
|
||||
fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare()
|
||||
|
||||
fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare()
|
||||
@ -78,4 +84,11 @@ interface MangaQueries : DbProvider {
|
||||
.build())
|
||||
.prepare()
|
||||
|
||||
fun getLastReadManga() = db.get()
|
||||
.listOfObjects(Manga::class.java)
|
||||
.withQuery(RawQuery.builder()
|
||||
.query(getLastReadMangaQuery())
|
||||
.observesTables(MangaTable.TABLE)
|
||||
.build())
|
||||
.prepare()
|
||||
}
|
@ -73,6 +73,18 @@ fun getHistoryByMangaId() = """
|
||||
WHERE ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = ? AND ${History.TABLE}.${History.COL_CHAPTER_ID} = ${Chapter.TABLE}.${Chapter.COL_ID}
|
||||
"""
|
||||
|
||||
fun getLastReadMangaQuery() = """
|
||||
SELECT ${Manga.TABLE}.*, MAX(${History.TABLE}.${History.COL_LAST_READ}) AS max
|
||||
FROM ${Manga.TABLE}
|
||||
JOIN ${Chapter.TABLE}
|
||||
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
|
||||
JOIN ${History.TABLE}
|
||||
ON ${Chapter.TABLE}.${Chapter.COL_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}
|
||||
WHERE ${Manga.TABLE}.${Manga.COL_FAVORITE} = 1
|
||||
GROUP BY ${Manga.TABLE}.${Manga.COL_ID}
|
||||
ORDER BY max DESC
|
||||
"""
|
||||
|
||||
/**
|
||||
* Query to get the categories for a manga.
|
||||
*/
|
||||
|
@ -0,0 +1,33 @@
|
||||
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 MangaLastUpdatedPutResolver : 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_LAST_UPDATE, manga.last_update)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -110,6 +110,15 @@ class DownloadManager(context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the directory name for a manga.
|
||||
*
|
||||
* @param manga the manga to query.
|
||||
*/
|
||||
fun getMangaDirName(manga: Manga): String {
|
||||
return provider.getMangaDirName(manga)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the directory name for the given chapter.
|
||||
*
|
||||
@ -119,6 +128,15 @@ class DownloadManager(context: Context) {
|
||||
return provider.getChapterDirName(chapter)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the download directory for a source if it exists.
|
||||
*
|
||||
* @param source the source to query.
|
||||
*/
|
||||
fun findSourceDir(source: Source): UniFile? {
|
||||
return provider.findSourceDir(source)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the directory for the given manga, if it exists.
|
||||
*
|
||||
|
@ -6,6 +6,7 @@ import com.hippo.unifile.UniFile
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.data.source.Source
|
||||
import eu.kanade.tachiyomi.util.DiskUtil
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
@ -26,10 +27,13 @@ class DownloadProvider(private val context: Context) {
|
||||
/**
|
||||
* The root directory for downloads.
|
||||
*/
|
||||
private lateinit var downloadsDir: UniFile
|
||||
private var downloadsDir = preferences.downloadsDirectory().getOrDefault().let {
|
||||
UniFile.fromUri(context, Uri.parse(it))
|
||||
}
|
||||
|
||||
init {
|
||||
preferences.downloadsDirectory().asObservable()
|
||||
.skip(1)
|
||||
.subscribe { downloadsDir = UniFile.fromUri(context, Uri.parse(it)) }
|
||||
}
|
||||
|
||||
@ -45,6 +49,15 @@ class DownloadProvider(private val context: Context) {
|
||||
.createDirectory(getMangaDirName(manga))
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the download directory for a source if it exists.
|
||||
*
|
||||
* @param source the source to query.
|
||||
*/
|
||||
fun findSourceDir(source: Source): UniFile? {
|
||||
return downloadsDir.findFile(getSourceDirName(source))
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the download directory for a manga if it exists.
|
||||
*
|
||||
@ -52,7 +65,7 @@ class DownloadProvider(private val context: Context) {
|
||||
* @param manga the manga to query.
|
||||
*/
|
||||
fun findMangaDir(source: Source, manga: Manga): UniFile? {
|
||||
val sourceDir = downloadsDir.findFile(getSourceDirName(source))
|
||||
val sourceDir = findSourceDir(source)
|
||||
return sourceDir?.findFile(getMangaDirName(manga))
|
||||
}
|
||||
|
||||
|
@ -13,7 +13,10 @@ import eu.kanade.tachiyomi.Constants
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Category
|
||||
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.DownloadService
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
@ -53,6 +56,8 @@ class LibraryUpdateService : Service() {
|
||||
*/
|
||||
val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
val downloadManager: DownloadManager by injectLazy()
|
||||
|
||||
/**
|
||||
* Wake lock that will be held until the service is destroyed.
|
||||
*/
|
||||
@ -243,32 +248,55 @@ class LibraryUpdateService : Service() {
|
||||
// If there's any error, return empty update and continue.
|
||||
.onErrorReturn {
|
||||
failedUpdates.add(manga)
|
||||
Pair(0, 0)
|
||||
Pair(emptyList<Chapter>(), emptyList<Chapter>())
|
||||
}
|
||||
// Filter out mangas without new chapters (or failed).
|
||||
.filter { pair -> pair.first > 0 }
|
||||
.filter { pair -> pair.first.size > 0 }
|
||||
.doOnNext {
|
||||
if (preferences.downloadNew()) {
|
||||
downloadChapters(manga, it.first)
|
||||
}
|
||||
}
|
||||
// Convert to the manga that contains new chapters.
|
||||
.map { manga }
|
||||
}
|
||||
// Add manga with new chapters to the list.
|
||||
.doOnNext { newUpdates.add(it) }
|
||||
.doOnNext { manga ->
|
||||
// Set last updated time
|
||||
manga.last_update = Date().time
|
||||
db.updateLastUpdated(manga).executeAsBlocking()
|
||||
// Add to the list
|
||||
newUpdates.add(manga)
|
||||
}
|
||||
// Notify result of the overall update.
|
||||
.doOnCompleted {
|
||||
if (newUpdates.isEmpty()) {
|
||||
cancelNotification()
|
||||
} else {
|
||||
if (preferences.downloadNew()) {
|
||||
DownloadService.start(this)
|
||||
}
|
||||
showResultNotification(newUpdates, failedUpdates)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
|
||||
// we need to get the chapters from the db so we have chapter ids
|
||||
val mangaChapters = db.getChapters(manga).executeAsBlocking()
|
||||
val dbChapters = chapters.map {
|
||||
mangaChapters.find { mangaChapter -> mangaChapter.url == it.url }!!
|
||||
}
|
||||
downloadManager.downloadChapters(manga, dbChapters)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the chapters for the given manga and adds them to the database.
|
||||
*
|
||||
* @param manga the manga to update.
|
||||
* @return a pair of the inserted and removed chapters.
|
||||
*/
|
||||
fun updateManga(manga: Manga): Observable<Pair<Int, Int>> {
|
||||
fun updateManga(manga: Manga): Observable<Pair<List<Chapter>, List<Chapter>>> {
|
||||
val source = sourceManager.get(manga.source) as? OnlineSource ?: return Observable.empty()
|
||||
return source.fetchChapterList(manga)
|
||||
.map { syncChaptersWithSource(db, it, manga, source) }
|
||||
|
@ -46,6 +46,15 @@ fun Call.asObservable(): Observable<Response> {
|
||||
}
|
||||
}
|
||||
|
||||
fun Call.asObservableSuccess(): Observable<Response> {
|
||||
return asObservable().doOnNext { response ->
|
||||
if (!response.isSuccessful) {
|
||||
response.close()
|
||||
throw Exception("Unsuccessful code ${response.code()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
|
||||
val progressClient = newBuilder()
|
||||
.cache(null)
|
||||
|
@ -83,10 +83,14 @@ class PreferenceKeys(context: Context) {
|
||||
|
||||
val filterUnread = context.getString(R.string.pref_filter_unread_key)
|
||||
|
||||
val librarySortingMode = context.getString(R.string.pref_library_sorting_mode_key)
|
||||
|
||||
val automaticUpdates = context.getString(R.string.pref_enable_automatic_updates_key)
|
||||
|
||||
val startScreen = context.getString(R.string.pref_start_screen_key)
|
||||
|
||||
val downloadNew = context.getString(R.string.pref_download_new_key)
|
||||
|
||||
fun sourceUsername(sourceId: Int) = "pref_source_username_$sourceId"
|
||||
|
||||
fun sourcePassword(sourceId: Int) = "pref_source_password_$sourceId"
|
||||
@ -97,4 +101,6 @@ class PreferenceKeys(context: Context) {
|
||||
|
||||
val libraryAsList = context.getString(R.string.pref_display_library_as_list)
|
||||
|
||||
val lang = context.getString(R.string.pref_language_key)
|
||||
|
||||
}
|
||||
|
@ -13,6 +13,8 @@ import java.io.File
|
||||
|
||||
fun <T> Preference<T>.getOrDefault(): T = get() ?: defaultValue()!!
|
||||
|
||||
fun Preference<Boolean>.invert(): Boolean = getOrDefault().let { set(!it); !it }
|
||||
|
||||
class PreferencesHelper(context: Context) {
|
||||
|
||||
val keys = PreferenceKeys(context)
|
||||
@ -126,8 +128,16 @@ class PreferencesHelper(context: Context) {
|
||||
|
||||
fun filterUnread() = rxPrefs.getBoolean(keys.filterUnread, false)
|
||||
|
||||
fun librarySortingMode() = rxPrefs.getInteger(keys.librarySortingMode, 0)
|
||||
|
||||
fun librarySortingAscending() = rxPrefs.getBoolean("library_sorting_ascending", true)
|
||||
|
||||
fun automaticUpdates() = prefs.getBoolean(keys.automaticUpdates, false)
|
||||
|
||||
fun hiddenCatalogues() = rxPrefs.getStringSet("hidden_catalogues", emptySet())
|
||||
|
||||
fun downloadNew() = prefs.getBoolean(keys.downloadNew, false)
|
||||
|
||||
fun lang() = prefs.getInt(keys.lang, 0)
|
||||
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.network.GET
|
||||
import eu.kanade.tachiyomi.data.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.data.network.asObservable
|
||||
import eu.kanade.tachiyomi.data.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.data.network.newCallWithProgress
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.source.Language
|
||||
@ -93,7 +93,7 @@ abstract class OnlineSource() : Source {
|
||||
*/
|
||||
open fun fetchPopularManga(page: MangasPage): Observable<MangasPage> = client
|
||||
.newCall(popularMangaRequest(page))
|
||||
.asObservable()
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
popularMangaParse(response, page)
|
||||
page
|
||||
@ -136,7 +136,7 @@ abstract class OnlineSource() : Source {
|
||||
*/
|
||||
open fun fetchSearchManga(page: MangasPage, query: String, filters: List<Filter>): Observable<MangasPage> = client
|
||||
.newCall(searchMangaRequest(page, query, filters))
|
||||
.asObservable()
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
searchMangaParse(response, page, query, filters)
|
||||
page
|
||||
@ -178,7 +178,7 @@ abstract class OnlineSource() : Source {
|
||||
*/
|
||||
open fun fetchLatestUpdates(page: MangasPage): Observable<MangasPage> = client
|
||||
.newCall(latestUpdatesRequest(page))
|
||||
.asObservable()
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
latestUpdatesParse(response, page)
|
||||
page
|
||||
@ -212,7 +212,7 @@ abstract class OnlineSource() : Source {
|
||||
*/
|
||||
override fun fetchMangaDetails(manga: Manga): Observable<Manga> = client
|
||||
.newCall(mangaDetailsRequest(manga))
|
||||
.asObservable()
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
Manga.create(manga.url, id).apply {
|
||||
mangaDetailsParse(response, this)
|
||||
@ -246,7 +246,7 @@ abstract class OnlineSource() : Source {
|
||||
*/
|
||||
override fun fetchChapterList(manga: Manga): Observable<List<Chapter>> = client
|
||||
.newCall(chapterListRequest(manga))
|
||||
.asObservable()
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
mutableListOf<Chapter>().apply {
|
||||
chapterListParse(response, this)
|
||||
@ -292,11 +292,8 @@ abstract class OnlineSource() : Source {
|
||||
*/
|
||||
open fun fetchPageListFromNetwork(chapter: Chapter): Observable<List<Page>> = client
|
||||
.newCall(pageListRequest(chapter))
|
||||
.asObservable()
|
||||
.asObservableSuccess()
|
||||
.map { response ->
|
||||
if (!response.isSuccessful) {
|
||||
throw Exception("Webpage sent ${response.code()} code")
|
||||
}
|
||||
mutableListOf<Page>().apply {
|
||||
pageListParse(response, this)
|
||||
if (isEmpty()) {
|
||||
@ -338,7 +335,7 @@ abstract class OnlineSource() : Source {
|
||||
page.status = Page.LOAD_PAGE
|
||||
return client
|
||||
.newCall(imageUrlRequest(page))
|
||||
.asObservable()
|
||||
.asObservableSuccess()
|
||||
.map { imageUrlParse(it) }
|
||||
.doOnError { page.status = Page.ERROR }
|
||||
.onErrorReturn { null }
|
||||
@ -381,13 +378,7 @@ abstract class OnlineSource() : Source {
|
||||
*/
|
||||
fun imageResponse(page: Page): Observable<Response> = client
|
||||
.newCallWithProgress(imageRequest(page), page)
|
||||
.asObservable()
|
||||
.doOnNext {
|
||||
if (!it.isSuccessful) {
|
||||
it.close()
|
||||
throw RuntimeException("Not a valid response")
|
||||
}
|
||||
}
|
||||
.asObservableSuccess()
|
||||
|
||||
/**
|
||||
* Returns the request for getting the source image. Override only if it's needed to override
|
||||
|
@ -1,9 +1,27 @@
|
||||
package eu.kanade.tachiyomi.ui.base.activity
|
||||
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import eu.kanade.tachiyomi.util.LocaleHelper
|
||||
|
||||
abstract class BaseActivity : AppCompatActivity(), ActivityMixin {
|
||||
|
||||
init {
|
||||
LocaleHelper.updateCfg(this)
|
||||
}
|
||||
|
||||
override fun getActivity() = this
|
||||
|
||||
var resumed = false
|
||||
private set
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
resumed = true
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
resumed = false
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -3,10 +3,15 @@ package eu.kanade.tachiyomi.ui.base.activity
|
||||
import android.os.Bundle
|
||||
import eu.kanade.tachiyomi.App
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.util.LocaleHelper
|
||||
import nucleus.view.NucleusAppCompatActivity
|
||||
|
||||
abstract class BaseRxActivity<P : BasePresenter<*>> : NucleusAppCompatActivity<P>(), ActivityMixin {
|
||||
|
||||
init {
|
||||
LocaleHelper.updateCfg(this)
|
||||
}
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
val superFactory = presenterFactory
|
||||
setPresenterFactory {
|
||||
@ -20,4 +25,17 @@ abstract class BaseRxActivity<P : BasePresenter<*>> : NucleusAppCompatActivity<P
|
||||
|
||||
override fun getActivity() = this
|
||||
|
||||
var resumed = false
|
||||
private set
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
resumed = true
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
resumed = false
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -221,6 +221,7 @@ open class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleVie
|
||||
|
||||
// Setup filters button
|
||||
menu.findItem(R.id.action_set_filter).apply {
|
||||
icon.mutate()
|
||||
if (presenter.source.filters.isEmpty()) {
|
||||
isEnabled = false
|
||||
icon.alpha = 128
|
||||
|
@ -21,7 +21,7 @@ import rx.schedulers.Schedulers
|
||||
import rx.subjects.PublishSubject
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.NoSuchElementException
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Presenter of [CatalogueFragment].
|
||||
|
@ -27,7 +27,9 @@ import nucleus.factory.RequiresPresenter
|
||||
* UI related actions should be called from here.
|
||||
*/
|
||||
@RequiresPresenter(CategoryPresenter::class)
|
||||
class CategoryActivity : BaseRxActivity<CategoryPresenter>(), ActionMode.Callback, FlexibleViewHolder.OnListItemClickListener, OnStartDragListener {
|
||||
class CategoryActivity :
|
||||
BaseRxActivity<CategoryPresenter>(),
|
||||
ActionMode.Callback, FlexibleViewHolder.OnListItemClickListener, OnStartDragListener {
|
||||
|
||||
/**
|
||||
* Object used to show actionMode toolbar.
|
||||
|
@ -1,7 +1,6 @@
|
||||
package eu.kanade.tachiyomi.ui.category
|
||||
|
||||
import android.view.ViewGroup
|
||||
import com.amulyakhare.textdrawable.util.ColorGenerator
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Category
|
||||
@ -17,18 +16,10 @@ import java.util.*
|
||||
* @param activity activity that created adapter
|
||||
* @constructor Creates a CategoryAdapter object
|
||||
*/
|
||||
class CategoryAdapter(private val activity: CategoryActivity) : FlexibleAdapter<CategoryHolder, Category>(), ItemTouchHelperAdapter {
|
||||
|
||||
/**
|
||||
* Generator used to generate circle letter icons
|
||||
*/
|
||||
private val generator: ColorGenerator
|
||||
class CategoryAdapter(private val activity: CategoryActivity) :
|
||||
FlexibleAdapter<CategoryHolder, Category>(), ItemTouchHelperAdapter {
|
||||
|
||||
init {
|
||||
// Let generator use Material Design colors.
|
||||
// Material design is love, material design is live!
|
||||
generator = ColorGenerator.MATERIAL
|
||||
|
||||
// Set unique id's
|
||||
setHasStableIds(true)
|
||||
}
|
||||
@ -54,7 +45,7 @@ class CategoryAdapter(private val activity: CategoryActivity) : FlexibleAdapter<
|
||||
override fun onBindViewHolder(holder: CategoryHolder, position: Int) {
|
||||
// Update holder values.
|
||||
val category = getItem(position)
|
||||
holder.onSetValues(category, generator)
|
||||
holder.onSetValues(category)
|
||||
|
||||
//When user scrolls this bind the correct selection status
|
||||
holder.itemView.isActivated = isSelected(position)
|
||||
|
@ -24,7 +24,12 @@ import kotlinx.android.synthetic.main.item_edit_categories.view.*
|
||||
*
|
||||
* @constructor Create CategoryHolder object
|
||||
*/
|
||||
class CategoryHolder(view: View, adapter: CategoryAdapter, listener: FlexibleViewHolder.OnListItemClickListener, dragListener: OnStartDragListener) : FlexibleViewHolder(view, adapter, listener) {
|
||||
class CategoryHolder(
|
||||
view: View,
|
||||
adapter: CategoryAdapter,
|
||||
listener: FlexibleViewHolder.OnListItemClickListener,
|
||||
dragListener: OnStartDragListener
|
||||
) : FlexibleViewHolder(view, adapter, listener) {
|
||||
|
||||
init {
|
||||
// Create round letter image onclick to simulate long click
|
||||
@ -46,29 +51,31 @@ class CategoryHolder(view: View, adapter: CategoryAdapter, listener: FlexibleVie
|
||||
* Update category item values.
|
||||
*
|
||||
* @param category category of item.
|
||||
* @param generator generator used to generate circle letter icons.
|
||||
*/
|
||||
fun onSetValues(category: Category, generator: ColorGenerator) {
|
||||
fun onSetValues(category: Category) {
|
||||
// Set capitalized title.
|
||||
itemView.title.text = category.name.capitalize()
|
||||
|
||||
// Update circle letter image.
|
||||
itemView.image.setImageDrawable(getRound(category.name.substring(0, 1).toUpperCase(), generator))
|
||||
itemView.post {
|
||||
itemView.image.setImageDrawable(getRound(category.name.take(1).toUpperCase()))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns circle letter image
|
||||
*
|
||||
* @param text first letter of string
|
||||
* @param generator the generator used to generate circle letter image
|
||||
*/
|
||||
private fun getRound(text: String, generator: ColorGenerator): TextDrawable {
|
||||
private fun getRound(text: String): TextDrawable {
|
||||
val size = Math.min(itemView.image.width, itemView.image.height)
|
||||
return TextDrawable.builder()
|
||||
.beginConfig()
|
||||
.width(size)
|
||||
.height(size)
|
||||
.textColor(Color.WHITE)
|
||||
.useFont(Typeface.DEFAULT)
|
||||
.toUpperCase()
|
||||
.endConfig()
|
||||
.buildRound(text, generator.getColor(text))
|
||||
.buildRound(text, ColorGenerator.MATERIAL.getColor(text))
|
||||
}
|
||||
}
|
@ -48,8 +48,9 @@ class DownloadHolder(private val view: View) : RecyclerView.ViewHolder(view) {
|
||||
* Updates the progress bar of the download.
|
||||
*/
|
||||
fun notifyProgress() {
|
||||
val pages = download.pages ?: return
|
||||
if (view.download_progress.max == 1) {
|
||||
view.download_progress.max = download.pages!!.size * 100
|
||||
view.download_progress.max = pages.size * 100
|
||||
}
|
||||
view.download_progress.progress = download.totalProgress
|
||||
}
|
||||
@ -58,7 +59,8 @@ class DownloadHolder(private val view: View) : RecyclerView.ViewHolder(view) {
|
||||
* Updates the text field of the number of downloaded pages.
|
||||
*/
|
||||
fun notifyDownloadedPages() {
|
||||
view.download_progress_text.text = "${download.downloadedImages}/${download.pages!!.size}"
|
||||
val pages = download.pages ?: return
|
||||
view.download_progress_text.text = "${download.downloadedImages}/${pages.size}"
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ class LibraryCategoryAdapter(val fragment: LibraryCategoryView) :
|
||||
/**
|
||||
* The list of manga in this category.
|
||||
*/
|
||||
private var mangas: List<Manga>? = null
|
||||
private var mangas: List<Manga> = emptyList()
|
||||
|
||||
init {
|
||||
setHasStableIds(true)
|
||||
@ -37,7 +37,7 @@ class LibraryCategoryAdapter(val fragment: LibraryCategoryView) :
|
||||
fun setItems(list: List<Manga>) {
|
||||
mItems = list
|
||||
|
||||
// A copy of manga that it's always unfiltered
|
||||
// A copy of manga always unfiltered.
|
||||
mangas = ArrayList(list)
|
||||
updateDataSet(null)
|
||||
}
|
||||
@ -58,10 +58,8 @@ class LibraryCategoryAdapter(val fragment: LibraryCategoryView) :
|
||||
* @param param the filter. Not used.
|
||||
*/
|
||||
override fun updateDataSet(param: String?) {
|
||||
mangas?.let {
|
||||
filterItems(it)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
filterItems(mangas)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -3,9 +3,12 @@ package eu.kanade.tachiyomi.ui.library
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.support.design.widget.TabLayout
|
||||
import android.support.v4.graphics.drawable.DrawableCompat
|
||||
import android.support.v4.view.ViewPager
|
||||
import android.support.v4.widget.DrawerLayout
|
||||
import android.support.v7.view.ActionMode
|
||||
import android.support.v7.widget.SearchView
|
||||
import android.view.*
|
||||
@ -20,6 +23,7 @@ import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
|
||||
import eu.kanade.tachiyomi.ui.category.CategoryActivity
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||
import eu.kanade.tachiyomi.util.inflate
|
||||
import eu.kanade.tachiyomi.util.toast
|
||||
import kotlinx.android.synthetic.main.activity_main.*
|
||||
import kotlinx.android.synthetic.main.fragment_library.*
|
||||
@ -73,22 +77,36 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
|
||||
*/
|
||||
private var selectedCoverManga: Manga? = null
|
||||
|
||||
/**
|
||||
* Status of isFilterDownloaded
|
||||
*/
|
||||
var isFilterDownloaded = false
|
||||
|
||||
/**
|
||||
* Status of isFilterUnread
|
||||
*/
|
||||
var isFilterUnread = false
|
||||
|
||||
/**
|
||||
* Number of manga per row in grid mode.
|
||||
*/
|
||||
var mangaPerRow = 0
|
||||
private set
|
||||
|
||||
/**
|
||||
* Navigation view containing filter/sort/display items.
|
||||
*/
|
||||
private lateinit var navView: LibraryNavigationView
|
||||
|
||||
/**
|
||||
* Drawer listener to allow swipe only for closing the drawer.
|
||||
*/
|
||||
private val drawerListener by lazy {
|
||||
object : DrawerLayout.SimpleDrawerListener() {
|
||||
override fun onDrawerClosed(drawerView: View) {
|
||||
if (drawerView == navView) {
|
||||
activity.drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, navView)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDrawerOpened(drawerView: View) {
|
||||
if (drawerView == navView) {
|
||||
activity.drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED, navView)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscription for the number of manga per row.
|
||||
*/
|
||||
@ -123,8 +141,6 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
setHasOptionsMenu(true)
|
||||
isFilterDownloaded = preferences.filterDownloaded().get() as Boolean
|
||||
isFilterUnread = preferences.filterUnread().get() as Boolean
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View? {
|
||||
@ -146,7 +162,7 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
|
||||
if (savedState != null) {
|
||||
activeCategory = savedState.getInt(CATEGORY_KEY)
|
||||
query = savedState.getString(QUERY_KEY)
|
||||
presenter.searchSubject.onNext(query)
|
||||
presenter.searchSubject.call(query)
|
||||
if (presenter.selectedMangas.isNotEmpty()) {
|
||||
createActionModeIfNeeded()
|
||||
}
|
||||
@ -159,6 +175,25 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
|
||||
.skip(1)
|
||||
// Set again the adapter to recalculate the covers height
|
||||
.subscribe { reattachAdapter() }
|
||||
|
||||
|
||||
// Inflate and prepare drawer
|
||||
navView = activity.drawer.inflate(R.layout.library_drawer) as LibraryNavigationView
|
||||
activity.drawer.addView(navView)
|
||||
activity.drawer.addDrawerListener(drawerListener)
|
||||
|
||||
navView.post {
|
||||
if (isAdded && !activity.drawer.isDrawerOpen(navView))
|
||||
activity.drawer.setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED, navView)
|
||||
}
|
||||
|
||||
navView.onGroupClicked = { group ->
|
||||
when (group) {
|
||||
is LibraryNavigationView.FilterGroup -> onFilterChanged()
|
||||
is LibraryNavigationView.SortGroup -> onSortChanged()
|
||||
is LibraryNavigationView.DisplayGroup -> reattachAdapter()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
@ -167,6 +202,8 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
activity.drawer.removeDrawerListener(drawerListener)
|
||||
activity.drawer.removeView(navView)
|
||||
numColumnsSubscription?.unsubscribe()
|
||||
tabs.setupWithViewPager(null)
|
||||
tabs.visibility = View.GONE
|
||||
@ -182,9 +219,6 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.library, menu)
|
||||
|
||||
// Initialize search menu
|
||||
val filterDownloadedItem = menu.findItem(R.id.action_filter_downloaded)
|
||||
val filterUnreadItem = menu.findItem(R.id.action_filter_unread)
|
||||
val searchItem = menu.findItem(R.id.action_search)
|
||||
val searchView = searchItem.actionView as SearchView
|
||||
|
||||
@ -194,8 +228,8 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
|
||||
searchView.clearFocus()
|
||||
}
|
||||
|
||||
filterDownloadedItem.isChecked = isFilterDownloaded
|
||||
filterUnreadItem.isChecked = isFilterUnread
|
||||
// Mutate the filter icon because it needs to be tinted and the resource is shared.
|
||||
menu.findItem(R.id.action_filter).icon.mutate()
|
||||
|
||||
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String): Boolean {
|
||||
@ -211,35 +245,19 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
|
||||
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
val filterItem = menu.findItem(R.id.action_filter)
|
||||
|
||||
// Tint icon if there's a filter active
|
||||
val filterColor = if (navView.hasActiveFilters()) Color.rgb(255, 238, 7) else Color.WHITE
|
||||
DrawableCompat.setTint(filterItem.icon, filterColor)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.action_filter_unread -> {
|
||||
// Change unread filter status.
|
||||
isFilterUnread = !isFilterUnread
|
||||
// Update settings.
|
||||
preferences.filterUnread().set(isFilterUnread)
|
||||
// Apply filter.
|
||||
onFilterCheckboxChanged()
|
||||
R.id.action_filter -> {
|
||||
activity.drawer.openDrawer(Gravity.END)
|
||||
}
|
||||
R.id.action_filter_downloaded -> {
|
||||
// Change downloaded filter status.
|
||||
isFilterDownloaded = !isFilterDownloaded
|
||||
// Update settings.
|
||||
preferences.filterDownloaded().set(isFilterDownloaded)
|
||||
// Apply filter.
|
||||
onFilterCheckboxChanged()
|
||||
}
|
||||
R.id.action_filter_empty -> {
|
||||
// Remove filter status.
|
||||
isFilterUnread = false
|
||||
isFilterDownloaded = false
|
||||
// Update settings.
|
||||
preferences.filterUnread().set(isFilterUnread)
|
||||
preferences.filterDownloaded().set(isFilterDownloaded)
|
||||
// Apply filter
|
||||
onFilterCheckboxChanged()
|
||||
}
|
||||
R.id.action_library_display_mode -> swapDisplayMode()
|
||||
R.id.action_update_library -> {
|
||||
LibraryUpdateService.start(activity)
|
||||
}
|
||||
@ -254,19 +272,18 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies filter change
|
||||
* Called when a filter is changed.
|
||||
*/
|
||||
private fun onFilterCheckboxChanged() {
|
||||
presenter.resubscribeLibrary()
|
||||
private fun onFilterChanged() {
|
||||
presenter.requestFilterUpdate()
|
||||
activity.supportInvalidateOptionsMenu()
|
||||
}
|
||||
|
||||
/**
|
||||
* Swap display mode
|
||||
* Called when the sorting mode is changed.
|
||||
*/
|
||||
private fun swapDisplayMode() {
|
||||
presenter.swapDisplayMode()
|
||||
reattachAdapter()
|
||||
private fun onSortChanged() {
|
||||
presenter.requestSortUpdate()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -302,7 +319,7 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
|
||||
|
||||
// Notify the subject the query has changed.
|
||||
if (isResumed) {
|
||||
presenter.searchSubject.onNext(query)
|
||||
presenter.searchSubject.call(query)
|
||||
}
|
||||
}
|
||||
|
||||
@ -330,7 +347,7 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
|
||||
view_pager.post { if (isAdded) tabs.setScrollPosition(view_pager.currentItem, 0f, true) }
|
||||
|
||||
// Send the manga map to child fragments after the adapter is updated.
|
||||
presenter.libraryMangaSubject.onNext(LibraryMangaEvent(mangaMap))
|
||||
presenter.libraryMangaSubject.call(LibraryMangaEvent(mangaMap))
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -0,0 +1,191 @@
|
||||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
|
||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.MultiSort.Companion.SORT_ASC
|
||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.MultiSort.Companion.SORT_DESC
|
||||
import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.MultiSort.Companion.SORT_NONE
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
/**
|
||||
* The navigation view shown in a drawer with the different options to show the library.
|
||||
*/
|
||||
class LibraryNavigationView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null)
|
||||
: ExtendedNavigationView(context, attrs) {
|
||||
|
||||
/**
|
||||
* Preferences helper.
|
||||
*/
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
/**
|
||||
* List of groups shown in the view.
|
||||
*/
|
||||
private val groups = listOf(FilterGroup(), SortGroup(), DisplayGroup())
|
||||
|
||||
/**
|
||||
* Adapter instance.
|
||||
*/
|
||||
private val adapter = Adapter(groups.map { it.createItems() }.flatten())
|
||||
|
||||
/**
|
||||
* Click listener to notify the parent fragment when an item from a group is clicked.
|
||||
*/
|
||||
var onGroupClicked: (Group) -> Unit = {}
|
||||
|
||||
init {
|
||||
recycler.adapter = adapter
|
||||
|
||||
groups.forEach { it.initModels() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if there's at least one filter from [FilterGroup] active.
|
||||
*/
|
||||
fun hasActiveFilters(): Boolean {
|
||||
return (groups[0] as FilterGroup).items.any { it.checked }
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapter of the recycler view.
|
||||
*/
|
||||
inner class Adapter(items: List<Item>) : ExtendedNavigationView.Adapter(items) {
|
||||
|
||||
override fun onItemClicked(item: Item) {
|
||||
if (item is GroupedItem) {
|
||||
item.group.onItemClicked(item)
|
||||
onGroupClicked(item.group)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters group (unread, downloaded, ...).
|
||||
*/
|
||||
inner class FilterGroup : Group {
|
||||
|
||||
private val downloaded = Item.CheckboxGroup(R.string.action_filter_downloaded, this)
|
||||
|
||||
private val unread = Item.CheckboxGroup(R.string.action_filter_unread, this)
|
||||
|
||||
override val items = listOf(downloaded, unread)
|
||||
|
||||
override val header = Item.Header(R.string.action_filter)
|
||||
|
||||
override val footer = Item.Separator()
|
||||
|
||||
override fun initModels() {
|
||||
downloaded.checked = preferences.filterDownloaded().getOrDefault()
|
||||
unread.checked = preferences.filterUnread().getOrDefault()
|
||||
}
|
||||
|
||||
override fun onItemClicked(item: Item) {
|
||||
item as Item.CheckboxGroup
|
||||
item.checked = !item.checked
|
||||
when (item) {
|
||||
downloaded -> preferences.filterDownloaded().set(item.checked)
|
||||
unread -> preferences.filterUnread().set(item.checked)
|
||||
}
|
||||
|
||||
adapter.notifyItemChanged(item)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Sorting group (alphabetically, by last read, ...) and ascending or descending.
|
||||
*/
|
||||
inner class SortGroup : Group {
|
||||
|
||||
private val alphabetically = Item.MultiSort(R.string.action_sort_alpha, this)
|
||||
|
||||
private val lastRead = Item.MultiSort(R.string.action_sort_last_read, this)
|
||||
|
||||
private val lastUpdated = Item.MultiSort(R.string.action_sort_last_updated, this)
|
||||
|
||||
private val unread = Item.MultiSort(R.string.action_filter_unread, this)
|
||||
|
||||
override val items = listOf(alphabetically, lastRead, lastUpdated, unread)
|
||||
|
||||
override val header = Item.Header(R.string.action_sort)
|
||||
|
||||
override val footer = Item.Separator()
|
||||
|
||||
override fun initModels() {
|
||||
val sorting = preferences.librarySortingMode().getOrDefault()
|
||||
val order = if (preferences.librarySortingAscending().getOrDefault())
|
||||
SORT_ASC else SORT_DESC
|
||||
|
||||
alphabetically.state = if (sorting == LibrarySort.ALPHA) order else SORT_NONE
|
||||
lastRead.state = if (sorting == LibrarySort.LAST_READ) order else SORT_NONE
|
||||
lastUpdated.state = if (sorting == LibrarySort.LAST_UPDATED) order else SORT_NONE
|
||||
unread.state = if (sorting == LibrarySort.UNREAD) order else SORT_NONE
|
||||
}
|
||||
|
||||
override fun onItemClicked(item: Item) {
|
||||
item as Item.MultiStateGroup
|
||||
val prevState = item.state
|
||||
|
||||
item.group.items.forEach { (it as Item.MultiStateGroup).state = SORT_NONE }
|
||||
item.state = when (prevState) {
|
||||
SORT_NONE -> SORT_ASC
|
||||
SORT_ASC -> SORT_DESC
|
||||
SORT_DESC -> SORT_ASC
|
||||
else -> throw Exception("Unknown state")
|
||||
}
|
||||
|
||||
preferences.librarySortingMode().set(when (item) {
|
||||
alphabetically -> LibrarySort.ALPHA
|
||||
lastRead -> LibrarySort.LAST_READ
|
||||
lastUpdated -> LibrarySort.LAST_UPDATED
|
||||
unread -> LibrarySort.UNREAD
|
||||
else -> throw Exception("Unknown sorting")
|
||||
})
|
||||
preferences.librarySortingAscending().set(if (item.state == SORT_ASC) true else false)
|
||||
|
||||
item.group.items.forEach { adapter.notifyItemChanged(it) }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Display group, to show the library as a list or a grid.
|
||||
*/
|
||||
inner class DisplayGroup : Group {
|
||||
|
||||
private val grid = Item.Radio(R.string.action_display_grid, this)
|
||||
|
||||
private val list = Item.Radio(R.string.action_display_list, this)
|
||||
|
||||
override val items = listOf(grid, list)
|
||||
|
||||
override val header = Item.Header(R.string.action_display)
|
||||
|
||||
override val footer = null
|
||||
|
||||
override fun initModels() {
|
||||
val asList = preferences.libraryAsList().getOrDefault()
|
||||
grid.checked = !asList
|
||||
list.checked = asList
|
||||
}
|
||||
|
||||
override fun onItemClicked(item: Item) {
|
||||
item as Item.Radio
|
||||
if (item.checked) return
|
||||
|
||||
item.group.items.forEach { (it as Item.Radio).checked = false }
|
||||
item.checked = true
|
||||
|
||||
preferences.libraryAsList().set(if (item == list) true else false)
|
||||
|
||||
item.group.items.forEach { adapter.notifyItemChanged(it) }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -2,6 +2,9 @@ package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Pair
|
||||
import com.hippo.unifile.UniFile
|
||||
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
|
||||
@ -12,11 +15,12 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||
import eu.kanade.tachiyomi.data.source.SourceManager
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.util.combineLatest
|
||||
import eu.kanade.tachiyomi.util.isNullOrUnsubscribed
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import rx.subjects.BehaviorSubject
|
||||
import rx.subjects.PublishSubject
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
@ -27,6 +31,31 @@ import java.util.*
|
||||
*/
|
||||
class LibraryPresenter : BasePresenter<LibraryFragment>() {
|
||||
|
||||
/**
|
||||
* Database.
|
||||
*/
|
||||
private val db: DatabaseHelper by injectLazy()
|
||||
|
||||
/**
|
||||
* Preferences.
|
||||
*/
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
/**
|
||||
* Cover cache.
|
||||
*/
|
||||
private val coverCache: CoverCache by injectLazy()
|
||||
|
||||
/**
|
||||
* Source manager.
|
||||
*/
|
||||
private val sourceManager: SourceManager by injectLazy()
|
||||
|
||||
/**
|
||||
* Download manager.
|
||||
*/
|
||||
private val downloadManager: DownloadManager by injectLazy()
|
||||
|
||||
/**
|
||||
* Categories of the library.
|
||||
*/
|
||||
@ -40,61 +69,139 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
|
||||
/**
|
||||
* Search query of the library.
|
||||
*/
|
||||
val searchSubject: BehaviorSubject<String> = BehaviorSubject.create()
|
||||
val searchSubject: BehaviorRelay<String> = BehaviorRelay.create()
|
||||
|
||||
/**
|
||||
* Subject to notify the library's viewpager for updates.
|
||||
*/
|
||||
val libraryMangaSubject: BehaviorSubject<LibraryMangaEvent> = BehaviorSubject.create()
|
||||
val libraryMangaSubject: BehaviorRelay<LibraryMangaEvent> = BehaviorRelay.create()
|
||||
|
||||
/**
|
||||
* Subject to notify the UI of selection updates.
|
||||
*/
|
||||
val selectionSubject: PublishSubject<LibrarySelectionEvent> = PublishSubject.create()
|
||||
val selectionSubject: PublishRelay<LibrarySelectionEvent> = PublishRelay.create()
|
||||
|
||||
/**
|
||||
* Database.
|
||||
* Relay used to apply the UI filters to the last emission of the library.
|
||||
*/
|
||||
val db: DatabaseHelper by injectLazy()
|
||||
private val filterTriggerRelay = BehaviorRelay.create(Unit)
|
||||
|
||||
/**
|
||||
* Preferences.
|
||||
* Relay used to apply the selected sorting method to the last emission of the library.
|
||||
*/
|
||||
val preferences: PreferencesHelper by injectLazy()
|
||||
private val sortTriggerRelay = BehaviorRelay.create(Unit)
|
||||
|
||||
/**
|
||||
* Cover cache.
|
||||
* Library subscription.
|
||||
*/
|
||||
val coverCache: CoverCache by injectLazy()
|
||||
|
||||
/**
|
||||
* Source manager.
|
||||
*/
|
||||
val sourceManager: SourceManager by injectLazy()
|
||||
|
||||
/**
|
||||
* Download manager.
|
||||
*/
|
||||
val downloadManager: DownloadManager by injectLazy()
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Id of the restartable that listens for library updates.
|
||||
*/
|
||||
const val GET_LIBRARY = 1
|
||||
}
|
||||
private var librarySubscription: Subscription? = null
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
subscribeLibrary()
|
||||
}
|
||||
|
||||
restartableLatestCache(GET_LIBRARY,
|
||||
{ getLibraryObservable() },
|
||||
{ view, pair -> view.onNextLibraryUpdate(pair.first, pair.second) })
|
||||
/**
|
||||
* Subscribes to library if needed.
|
||||
*/
|
||||
fun subscribeLibrary() {
|
||||
if (librarySubscription.isNullOrUnsubscribed()) {
|
||||
librarySubscription = getLibraryObservable()
|
||||
.combineLatest(filterTriggerRelay.observeOn(Schedulers.io()),
|
||||
{ lib, tick -> Pair(lib.first, applyFilters(lib.second)) })
|
||||
.combineLatest(sortTriggerRelay.observeOn(Schedulers.io()),
|
||||
{ lib, tick -> Pair(lib.first, applySort(lib.second)) })
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeLatestCache({ view, pair ->
|
||||
view.onNextLibraryUpdate(pair.first, pair.second)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (savedState == null) {
|
||||
start(GET_LIBRARY)
|
||||
/**
|
||||
* Applies library filters to the given map of manga.
|
||||
*
|
||||
* @param map the map to filter.
|
||||
*/
|
||||
private fun applyFilters(map: Map<Int, List<Manga>>): Map<Int, List<Manga>> {
|
||||
// Cached list of downloaded manga directories given a source id.
|
||||
val mangaDirectories = mutableMapOf<Int, Array<UniFile>>()
|
||||
|
||||
// Cached list of downloaded chapter directories for a manga.
|
||||
val chapterDirectories = mutableMapOf<Long, Boolean>()
|
||||
|
||||
val filterDownloaded = preferences.filterDownloaded().getOrDefault()
|
||||
|
||||
val filterUnread = preferences.filterUnread().getOrDefault()
|
||||
|
||||
val filterFn: (Manga) -> Boolean = f@ { manga ->
|
||||
// Filter out manga without source.
|
||||
val source = sourceManager.get(manga.source) ?: return@f false
|
||||
|
||||
// Filter when there isn't unread chapters.
|
||||
if (filterUnread && manga.unread == 0) {
|
||||
return@f false
|
||||
}
|
||||
|
||||
// Filter when the download directory doesn't exist or is null.
|
||||
if (filterDownloaded) {
|
||||
val mangaDirs = mangaDirectories.getOrPut(source.id) {
|
||||
downloadManager.findSourceDir(source)?.listFiles() ?: emptyArray()
|
||||
}
|
||||
|
||||
val mangaDirName = downloadManager.getMangaDirName(manga)
|
||||
val mangaDir = mangaDirs.find { it.name == mangaDirName } ?: return@f false
|
||||
|
||||
val hasDirs = chapterDirectories.getOrPut(manga.id!!) {
|
||||
(mangaDir.listFiles() ?: emptyArray()).isNotEmpty()
|
||||
}
|
||||
if (!hasDirs) {
|
||||
return@f false
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
return map.mapValues { entry -> entry.value.filter(filterFn) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies library sorting to the given map of manga.
|
||||
*
|
||||
* @param map the map to sort.
|
||||
*/
|
||||
private fun applySort(map: Map<Int, List<Manga>>): Map<Int, List<Manga>> {
|
||||
val sortingMode = preferences.librarySortingMode().getOrDefault()
|
||||
|
||||
// TODO lazy initialization in kotlin 1.1
|
||||
var lastReadManga: Map<Long, Int>? = null
|
||||
if (sortingMode == LibrarySort.LAST_READ) {
|
||||
var counter = 0
|
||||
lastReadManga = db.getLastReadManga().executeAsBlocking()
|
||||
.associate { it.id!! to counter++ }
|
||||
}
|
||||
|
||||
val sortFn: (Manga, Manga) -> Int = { manga1, manga2 ->
|
||||
when (sortingMode) {
|
||||
LibrarySort.ALPHA -> manga1.title.compareTo(manga2.title)
|
||||
LibrarySort.LAST_READ -> {
|
||||
// Get index of manga, set equal to list if size unknown.
|
||||
val manga1LastRead = lastReadManga!![manga1.id!!] ?: lastReadManga!!.size
|
||||
val manga2LastRead = lastReadManga!![manga2.id!!] ?: lastReadManga!!.size
|
||||
manga1LastRead.compareTo(manga2LastRead)
|
||||
}
|
||||
LibrarySort.LAST_UPDATED -> manga2.last_update.compareTo(manga1.last_update)
|
||||
LibrarySort.UNREAD -> manga1.unread.compareTo(manga2.unread)
|
||||
else -> throw Exception("Unknown sorting mode")
|
||||
}
|
||||
}
|
||||
|
||||
val comparator = if (preferences.librarySortingAscending().getOrDefault())
|
||||
Comparator(sortFn)
|
||||
else
|
||||
Collections.reverseOrder(sortFn)
|
||||
|
||||
return map.mapValues { entry -> entry.value.sortedWith(comparator) }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -102,7 +209,7 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
|
||||
*
|
||||
* @return an observable of the categories and its manga.
|
||||
*/
|
||||
fun getLibraryObservable(): Observable<Pair<List<Category>, Map<Int, List<Manga>>>> {
|
||||
private fun getLibraryObservable(): Observable<Pair<List<Category>, Map<Int, List<Manga>>>> {
|
||||
return Observable.combineLatest(getCategoriesObservable(), getLibraryMangasObservable(),
|
||||
{ dbCategories, libraryManga ->
|
||||
val categories = if (libraryManga.containsKey(0))
|
||||
@ -113,7 +220,6 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
|
||||
this.categories = categories
|
||||
Pair(categories, libraryManga)
|
||||
})
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
/**
|
||||
@ -121,7 +227,7 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
|
||||
*
|
||||
* @return an observable of the categories.
|
||||
*/
|
||||
fun getCategoriesObservable(): Observable<List<Category>> {
|
||||
private fun getCategoriesObservable(): Observable<List<Category>> {
|
||||
return db.getCategories().asRxObservable()
|
||||
}
|
||||
|
||||
@ -131,76 +237,23 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
|
||||
* @return an observable containing a map with the category id as key and a list of manga as the
|
||||
* value.
|
||||
*/
|
||||
fun getLibraryMangasObservable(): Observable<Map<Int, List<Manga>>> {
|
||||
private fun getLibraryMangasObservable(): Observable<Map<Int, List<Manga>>> {
|
||||
return db.getLibraryMangas().asRxObservable()
|
||||
.flatMap { mangas ->
|
||||
Observable.from(mangas)
|
||||
// Filter library by options
|
||||
.filter { filterManga(it) }
|
||||
.groupBy { it.category }
|
||||
.flatMap { group -> group.toList().map { Pair(group.key, it) } }
|
||||
.toMap({ it.first }, { it.second })
|
||||
}
|
||||
.map { list -> list.groupBy { it.category } }
|
||||
}
|
||||
|
||||
/**
|
||||
* Resubscribes to library if needed.
|
||||
* Requests the library to be filtered.
|
||||
*/
|
||||
fun subscribeLibrary() {
|
||||
if (isUnsubscribed(GET_LIBRARY)) {
|
||||
start(GET_LIBRARY)
|
||||
}
|
||||
fun requestFilterUpdate() {
|
||||
filterTriggerRelay.call(Unit)
|
||||
}
|
||||
|
||||
/**
|
||||
* Resubscribes to library.
|
||||
* Requests the library to be sorted.
|
||||
*/
|
||||
fun resubscribeLibrary() {
|
||||
start(GET_LIBRARY)
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters an entry of the library.
|
||||
*
|
||||
* @param manga a favorite manga from the database.
|
||||
* @return true if the entry is included, false otherwise.
|
||||
*/
|
||||
fun filterManga(manga: Manga): Boolean {
|
||||
// Filter out manga without source
|
||||
val source = sourceManager.get(manga.source) ?: return false
|
||||
|
||||
val prefFilterDownloaded = preferences.filterDownloaded().getOrDefault()
|
||||
val prefFilterUnread = preferences.filterUnread().getOrDefault()
|
||||
|
||||
// Check if filter option is selected
|
||||
if (prefFilterDownloaded || prefFilterUnread) {
|
||||
|
||||
// Does it have downloaded chapters.
|
||||
var hasDownloaded = false
|
||||
var hasUnread = false
|
||||
|
||||
if (prefFilterUnread) {
|
||||
// Does it have unread chapters.
|
||||
hasUnread = manga.unread > 0
|
||||
}
|
||||
|
||||
if (prefFilterDownloaded) {
|
||||
val mangaDir = downloadManager.findMangaDir(source, manga)
|
||||
|
||||
if (mangaDir != null) {
|
||||
hasDownloaded = mangaDir.listFiles()?.any { it.isDirectory } ?: false
|
||||
}
|
||||
}
|
||||
|
||||
// Return correct filter status
|
||||
if (prefFilterDownloaded && prefFilterUnread) {
|
||||
return (hasDownloaded && hasUnread)
|
||||
} else {
|
||||
return (hasDownloaded || hasUnread)
|
||||
}
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
fun requestSortUpdate() {
|
||||
sortTriggerRelay.call(Unit)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -208,7 +261,7 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
|
||||
*/
|
||||
fun onOpenManga() {
|
||||
// Avoid further db updates for the library when it's not needed
|
||||
stop(GET_LIBRARY)
|
||||
librarySubscription?.let { remove(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
@ -220,10 +273,10 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
|
||||
fun setSelection(manga: Manga, selected: Boolean) {
|
||||
if (selected) {
|
||||
selectedMangas.add(manga)
|
||||
selectionSubject.onNext(LibrarySelectionEvent.Selected(manga))
|
||||
selectionSubject.call(LibrarySelectionEvent.Selected(manga))
|
||||
} else {
|
||||
selectedMangas.remove(manga)
|
||||
selectionSubject.onNext(LibrarySelectionEvent.Unselected(manga))
|
||||
selectionSubject.call(LibrarySelectionEvent.Unselected(manga))
|
||||
}
|
||||
}
|
||||
|
||||
@ -232,7 +285,7 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
|
||||
*/
|
||||
fun clearSelections() {
|
||||
selectedMangas.clear()
|
||||
selectionSubject.onNext(LibrarySelectionEvent.Cleared())
|
||||
selectionSubject.call(LibrarySelectionEvent.Cleared())
|
||||
}
|
||||
|
||||
/**
|
||||
@ -296,12 +349,4 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes the active display mode.
|
||||
*/
|
||||
fun swapDisplayMode() {
|
||||
val displayAsList = preferences.libraryAsList().getOrDefault()
|
||||
preferences.libraryAsList().set(!displayAsList)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,10 @@
|
||||
package eu.kanade.tachiyomi.ui.library
|
||||
|
||||
object LibrarySort {
|
||||
|
||||
const val ALPHA = 0
|
||||
const val LAST_READ = 1
|
||||
const val LAST_UPDATED = 2
|
||||
const val UNREAD = 3
|
||||
|
||||
}
|
@ -94,7 +94,9 @@ class MainActivity : BaseActivity() {
|
||||
override fun onBackPressed() {
|
||||
val fragment = supportFragmentManager.findFragmentById(R.id.frame_container)
|
||||
if (fragment != null && fragment.tag.toInt() != startScreenId) {
|
||||
setSelectedDrawerItem(startScreenId)
|
||||
if (resumed) {
|
||||
setSelectedDrawerItem(startScreenId)
|
||||
}
|
||||
} else {
|
||||
super.onBackPressed()
|
||||
}
|
||||
@ -110,6 +112,8 @@ class MainActivity : BaseActivity() {
|
||||
} else if (resultCode and SettingsActivity.FLAG_THEME_CHANGED != 0) {
|
||||
// Delay activity recreation to avoid fragment leaks.
|
||||
nav_view.post { recreate() }
|
||||
} else if (resultCode and SettingsActivity.FLAG_LANG_CHANGED != 0) {
|
||||
nav_view.post { recreate() }
|
||||
}
|
||||
} else {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersFragment
|
||||
import eu.kanade.tachiyomi.ui.manga.info.MangaInfoFragment
|
||||
import eu.kanade.tachiyomi.ui.manga.myanimelist.MyAnimeListFragment
|
||||
import eu.kanade.tachiyomi.util.SharedData
|
||||
import eu.kanade.tachiyomi.util.toast
|
||||
import kotlinx.android.synthetic.main.activity_manga.*
|
||||
import kotlinx.android.synthetic.main.toolbar.*
|
||||
import nucleus.factory.RequiresPresenter
|
||||
@ -50,12 +51,19 @@ class MangaActivity : BaseRxActivity<MangaPresenter>() {
|
||||
|
||||
val fromLauncher = intent.getBooleanExtra(FROM_LAUNCHER_EXTRA, false)
|
||||
|
||||
//Remove any current manga if we are launching from launcher
|
||||
if(fromLauncher) SharedData.remove(MangaEvent::class.java)
|
||||
// Remove any current manga if we are launching from launcher
|
||||
if (fromLauncher) SharedData.remove(MangaEvent::class.java)
|
||||
|
||||
presenter.setMangaEvent(SharedData.getOrPut(MangaEvent::class.java) {
|
||||
val id = intent.getLongExtra(MANGA_EXTRA, 0)
|
||||
MangaEvent(presenter.db.getManga(id).executeAsBlocking()!!)
|
||||
val dbManga = presenter.db.getManga(id).executeAsBlocking()
|
||||
if (dbManga != null) {
|
||||
MangaEvent(dbManga)
|
||||
} else {
|
||||
toast(R.string.manga_not_in_db)
|
||||
finish()
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
setupToolbar(toolbar)
|
||||
|
@ -394,7 +394,8 @@ class ChaptersFragment : BaseRxFragment<ChaptersPresenter>(), ActionMode.Callbac
|
||||
}
|
||||
|
||||
fun dismissDeletingDialog() {
|
||||
(childFragmentManager.findFragmentByTag(DeletingChaptersDialog.TAG) as? DialogFragment)?.dismiss()
|
||||
(childFragmentManager.findFragmentByTag(DeletingChaptersDialog.TAG) as? DialogFragment)
|
||||
?.dismissAllowingStateLoss()
|
||||
}
|
||||
|
||||
override fun onListItemClick(position: Int): Boolean {
|
||||
|
@ -189,7 +189,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
||||
KeyEvent.KEYCODE_VOLUME_DOWN -> {
|
||||
if (volumeKeysEnabled) {
|
||||
if (event.action == KeyEvent.ACTION_UP) {
|
||||
viewer?.moveToNext()
|
||||
viewer?.moveDown()
|
||||
}
|
||||
return true
|
||||
}
|
||||
@ -197,7 +197,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
||||
KeyEvent.KEYCODE_VOLUME_UP -> {
|
||||
if (volumeKeysEnabled) {
|
||||
if (event.action == KeyEvent.ACTION_UP) {
|
||||
viewer?.moveToPrevious()
|
||||
viewer?.moveUp()
|
||||
}
|
||||
return true
|
||||
}
|
||||
@ -210,12 +210,15 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
||||
override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {
|
||||
if (!isFinishing) {
|
||||
when (keyCode) {
|
||||
KeyEvent.KEYCODE_DPAD_RIGHT -> viewer?.moveToNext()
|
||||
KeyEvent.KEYCODE_DPAD_LEFT -> viewer?.moveToPrevious()
|
||||
KeyEvent.KEYCODE_DPAD_RIGHT -> viewer?.moveRight()
|
||||
KeyEvent.KEYCODE_DPAD_LEFT -> viewer?.moveLeft()
|
||||
KeyEvent.KEYCODE_DPAD_DOWN -> viewer?.moveDown()
|
||||
KeyEvent.KEYCODE_DPAD_UP -> viewer?.moveUp()
|
||||
KeyEvent.KEYCODE_MENU -> toggleMenu()
|
||||
else -> return super.onKeyUp(keyCode, event)
|
||||
}
|
||||
}
|
||||
return super.onKeyUp(keyCode, event)
|
||||
return true
|
||||
}
|
||||
|
||||
fun onChapterError(error: Throwable) {
|
||||
@ -224,14 +227,14 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
||||
toast(error.message)
|
||||
}
|
||||
|
||||
fun onLongPress(page: Page) {
|
||||
fun onLongClick(page: Page) {
|
||||
MaterialDialog.Builder(this)
|
||||
.title(getString(R.string.options))
|
||||
.items(R.array.reader_image_options)
|
||||
.itemsIds(R.array.reader_image_options_values)
|
||||
.itemsCallback { materialDialog, view, i, charSequence ->
|
||||
when (i) {
|
||||
0 -> presenter.setCover(page)
|
||||
0 -> setImageAsCover(page)
|
||||
1 -> shareImage(page)
|
||||
2 -> presenter.savePage(page)
|
||||
}
|
||||
@ -313,14 +316,14 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
||||
val mangaViewer = if (manga.viewer == 0) preferences.defaultViewer() else manga.viewer
|
||||
|
||||
// Try to reuse the viewer using its tag
|
||||
var fragment: BaseReader? = supportFragmentManager.findFragmentByTag(manga.viewer.toString()) as? BaseReader
|
||||
var fragment = supportFragmentManager.findFragmentByTag(manga.viewer.toString()) as? BaseReader
|
||||
if (fragment == null) {
|
||||
// Create a new viewer
|
||||
when (mangaViewer) {
|
||||
RIGHT_TO_LEFT -> fragment = RightToLeftReader()
|
||||
VERTICAL -> fragment = VerticalReader()
|
||||
WEBTOON -> fragment = WebtoonReader()
|
||||
else -> fragment = LeftToRightReader()
|
||||
fragment = when (mangaViewer) {
|
||||
RIGHT_TO_LEFT -> RightToLeftReader()
|
||||
VERTICAL -> VerticalReader()
|
||||
WEBTOON -> WebtoonReader()
|
||||
else -> LeftToRightReader()
|
||||
}
|
||||
|
||||
supportFragmentManager.beginTransaction().replace(R.id.reader, fragment, manga.viewer.toString()).commit()
|
||||
@ -469,23 +472,6 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a share intent that lets user share image
|
||||
*
|
||||
* @param page page object containing image information.
|
||||
*/
|
||||
fun shareImage(page: Page) {
|
||||
if (page.status != Page.READY)
|
||||
return
|
||||
|
||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||
putExtra(Intent.EXTRA_STREAM, page.uri)
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
type = "image/*"
|
||||
}
|
||||
startActivity(Intent.createChooser(intent, getString(R.string.action_share)))
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the brightness of the screen. Range is [-75, 100].
|
||||
* From -75 to -1 a semi-transparent black view is shown at the top with the minimum brightness.
|
||||
@ -570,4 +556,39 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a share intent that lets user share image
|
||||
*
|
||||
* @param page page object containing image information.
|
||||
*/
|
||||
private fun shareImage(page: Page) {
|
||||
if (page.status != Page.READY)
|
||||
return
|
||||
|
||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||
putExtra(Intent.EXTRA_STREAM, page.uri)
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
type = "image/*"
|
||||
}
|
||||
startActivity(Intent.createChooser(intent, getString(R.string.action_share)))
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the given page as the cover of the manga.
|
||||
*
|
||||
* @param page the page containing the image to set as cover.
|
||||
*/
|
||||
private fun setImageAsCover(page: Page) {
|
||||
if (page.status != Page.READY)
|
||||
return
|
||||
|
||||
MaterialDialog.Builder(this)
|
||||
.content(getString(R.string.confirm_set_image_as_cover))
|
||||
.positiveText(android.R.string.yes)
|
||||
.negativeText(android.R.string.no)
|
||||
.onPositive { dialog, which -> presenter.setImageAsCover(page) }
|
||||
.show()
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -523,19 +523,13 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
|
||||
/**
|
||||
* Update cover with page file.
|
||||
*/
|
||||
internal fun setCover(page: Page) {
|
||||
if (page.status != Page.READY)
|
||||
return
|
||||
|
||||
internal fun setImageAsCover(page: Page) {
|
||||
try {
|
||||
val thumbUrl = manga.thumbnail_url ?: throw Exception("Image url not found")
|
||||
if (manga.favorite) {
|
||||
if (manga.thumbnail_url != null) {
|
||||
val input = context.contentResolver.openInputStream(page.uri)
|
||||
coverCache.copyToCache(manga.thumbnail_url!!, input)
|
||||
context.toast(R.string.cover_updated)
|
||||
} else {
|
||||
throw Exception("Image url not found")
|
||||
}
|
||||
val input = context.contentResolver.openInputStream(page.uri)
|
||||
coverCache.copyToCache(thumbUrl, input)
|
||||
context.toast(R.string.cover_updated)
|
||||
} else {
|
||||
context.toast(R.string.notification_first_add_to_library)
|
||||
}
|
||||
|
@ -4,12 +4,11 @@ import android.app.PendingIntent
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.support.v4.content.FileProvider
|
||||
import eu.kanade.tachiyomi.BuildConfig
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.util.notificationManager
|
||||
import java.io.File
|
||||
import eu.kanade.tachiyomi.Constants.NOTIFICATION_DOWNLOAD_IMAGE_ID as defaultNotification
|
||||
|
||||
/**
|
||||
* The BroadcastReceiver of [ImageNotifier]
|
||||
@ -18,21 +17,16 @@ import java.io.File
|
||||
class ImageNotificationReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
when (intent.action) {
|
||||
ACTION_SHARE_IMAGE -> {
|
||||
shareImage(context, intent.getStringExtra(EXTRA_FILE_LOCATION))
|
||||
context.notificationManager.cancel(intent.getIntExtra(NOTIFICATION_ID, 5))
|
||||
}
|
||||
ACTION_SHOW_IMAGE ->
|
||||
showImage(context, intent.getStringExtra(EXTRA_FILE_LOCATION))
|
||||
ACTION_DELETE_IMAGE -> {
|
||||
deleteImage(intent.getStringExtra(EXTRA_FILE_LOCATION))
|
||||
context.notificationManager.cancel(intent.getIntExtra(NOTIFICATION_ID, 5))
|
||||
context.notificationManager.cancel(intent.getIntExtra(NOTIFICATION_ID, defaultNotification))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to delete image
|
||||
*
|
||||
* @param path path of file
|
||||
*/
|
||||
private fun deleteImage(path: String) {
|
||||
@ -40,60 +34,42 @@ class ImageNotificationReceiver : BroadcastReceiver() {
|
||||
if (file.exists()) file.delete()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to start share intent to share image
|
||||
* @param context context of application
|
||||
* @param path path of file
|
||||
*/
|
||||
private fun shareImage(context: Context, path: String) {
|
||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK
|
||||
putExtra(Intent.EXTRA_STREAM, Uri.parse(path))
|
||||
type = "image/*"
|
||||
}
|
||||
context.startActivity(Intent.createChooser(intent, context.getString(R.string.action_share)))
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to show image in gallery application
|
||||
* @param context context of application
|
||||
* @param path path of file
|
||||
*/
|
||||
private fun showImage(context: Context, path: String) {
|
||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
val uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", File(path))
|
||||
setDataAndType(uri, "image/*")
|
||||
}
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ACTION_SHARE_IMAGE = "eu.kanade.SHARE_IMAGE"
|
||||
|
||||
private const val ACTION_SHOW_IMAGE = "eu.kanade.SHOW_IMAGE"
|
||||
|
||||
private const val ACTION_DELETE_IMAGE = "eu.kanade.DELETE_IMAGE"
|
||||
|
||||
private const val EXTRA_FILE_LOCATION = "file_location"
|
||||
|
||||
private const val NOTIFICATION_ID = "notification_id"
|
||||
|
||||
internal fun shareImageIntent(context: Context, path: String, notificationId: Int): PendingIntent {
|
||||
val intent = Intent(context, ImageNotificationReceiver::class.java).apply {
|
||||
action = ACTION_SHARE_IMAGE
|
||||
putExtra(EXTRA_FILE_LOCATION, path)
|
||||
putExtra(NOTIFICATION_ID, notificationId)
|
||||
/**
|
||||
* Called to start share intent to share image
|
||||
*
|
||||
* @param context context of application
|
||||
* @param path path of file
|
||||
*/
|
||||
internal fun shareImageIntent(context: Context, path: String): PendingIntent {
|
||||
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||
val uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", File(path))
|
||||
putExtra(Intent.EXTRA_STREAM, uri)
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
type = "image/*"
|
||||
}
|
||||
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to show image in gallery application
|
||||
*
|
||||
* @param context context of application
|
||||
* @param path path of file
|
||||
*/
|
||||
internal fun showImageIntent(context: Context, path: String): PendingIntent {
|
||||
val intent = Intent(context, ImageNotificationReceiver::class.java).apply {
|
||||
action = ACTION_SHOW_IMAGE
|
||||
putExtra(EXTRA_FILE_LOCATION, path)
|
||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
val uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", File(path))
|
||||
setDataAndType(uri, "image/*")
|
||||
}
|
||||
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
|
||||
internal fun deleteImageIntent(context: Context, path: String, notificationId: Int): PendingIntent {
|
||||
|
@ -62,7 +62,7 @@ class ImageNotifier(private val context: Context) {
|
||||
// Share action
|
||||
addAction(R.drawable.ic_share_grey_24dp,
|
||||
context.getString(R.string.action_share),
|
||||
ImageNotificationReceiver.shareImageIntent(context, file.absolutePath, notificationId))
|
||||
ImageNotificationReceiver.shareImageIntent(context, file.absolutePath))
|
||||
// Delete action
|
||||
addAction(R.drawable.ic_delete_grey_24dp,
|
||||
context.getString(R.string.action_delete),
|
||||
|
@ -189,14 +189,38 @@ abstract class BaseReader : BaseFragment() {
|
||||
abstract fun onChapterAppended(chapter: ReaderChapter)
|
||||
|
||||
/**
|
||||
* Moves pages forward. Implementations decide how to move (by a page, by some distance...).
|
||||
* Moves pages to right. Implementations decide how to move (by a page, by some distance...).
|
||||
*/
|
||||
abstract fun moveToNext()
|
||||
abstract fun moveRight()
|
||||
|
||||
/**
|
||||
* Moves pages backward. Implementations decide how to move (by a page, by some distance...).
|
||||
* Moves pages to left. Implementations decide how to move (by a page, by some distance...).
|
||||
*/
|
||||
abstract fun moveToPrevious()
|
||||
abstract fun moveLeft()
|
||||
|
||||
/**
|
||||
* Moves pages down. Implementations decide how to move (by a page, by some distance...).
|
||||
*/
|
||||
open fun moveDown() {
|
||||
moveRight()
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves pages up. Implementations decide how to move (by a page, by some distance...).
|
||||
*/
|
||||
open fun moveUp() {
|
||||
moveLeft()
|
||||
}
|
||||
|
||||
/**
|
||||
* Method the implementations can call to show a menu with options for the given page.
|
||||
*/
|
||||
fun onLongClick(page: Page?): Boolean {
|
||||
if (isAdded && page != null) {
|
||||
readerActivity.onLongClick(page)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the active decoder class.
|
||||
|
@ -71,6 +71,7 @@ class PageView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
|
||||
setBitmapDecoderClass(reader.bitmapDecoderClass)
|
||||
setVerticalScrollingParent(reader is VerticalReader)
|
||||
setOnTouchListener { v, motionEvent -> reader.gestureDetector.onTouchEvent(motionEvent) }
|
||||
setOnLongClickListener { reader.onLongClick(page) }
|
||||
setOnImageEventListener(object : SubsamplingScaleImageView.DefaultOnImageEventListener() {
|
||||
override fun onReady() {
|
||||
onImageDecoded(reader)
|
||||
|
@ -66,7 +66,7 @@ abstract class PagerReader : BaseReader() {
|
||||
/**
|
||||
* Gesture detector for touch events.
|
||||
*/
|
||||
val gestureDetector by lazy { createGestureDetector() }
|
||||
val gestureDetector by lazy { GestureDetector(context, ImageGestureListener()) }
|
||||
|
||||
/**
|
||||
* Subscriptions for reader settings.
|
||||
@ -166,37 +166,24 @@ abstract class PagerReader : BaseReader() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the gesture detector for the pager.
|
||||
*
|
||||
* @return a gesture detector.
|
||||
* Gesture detector for Subsampling Scale Image View.
|
||||
*/
|
||||
protected fun createGestureDetector(): GestureDetector {
|
||||
return GestureDetector(activity, object : GestureDetector.SimpleOnGestureListener() {
|
||||
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
|
||||
if (isAdded) {
|
||||
val positionX = e.x
|
||||
inner class ImageGestureListener : GestureDetector.SimpleOnGestureListener() {
|
||||
|
||||
if (positionX < pager.width * LEFT_REGION) {
|
||||
if (tappingEnabled) onLeftSideTap()
|
||||
} else if (positionX > pager.width * RIGHT_REGION) {
|
||||
if (tappingEnabled) onRightSideTap()
|
||||
} else {
|
||||
readerActivity.toggleMenu()
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
|
||||
if (isAdded) {
|
||||
val positionX = e.x
|
||||
|
||||
override fun onLongPress(e: MotionEvent?) {
|
||||
if (isAdded) {
|
||||
val page = adapter.pages.getOrNull(pager.currentItem)
|
||||
if (page != null)
|
||||
readerActivity.onLongPress(page)
|
||||
else
|
||||
context.toast(getString(R.string.unknown_error))
|
||||
if (positionX < pager.width * LEFT_REGION) {
|
||||
if (tappingEnabled) moveLeft()
|
||||
} else if (positionX > pager.width * RIGHT_REGION) {
|
||||
if (tappingEnabled) moveRight()
|
||||
} else {
|
||||
readerActivity.toggleMenu()
|
||||
}
|
||||
}
|
||||
})
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -258,23 +245,23 @@ abstract class PagerReader : BaseReader() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the left side of the screen was clicked.
|
||||
* Moves a page to the right.
|
||||
*/
|
||||
protected open fun onLeftSideTap() {
|
||||
moveToPrevious()
|
||||
override fun moveRight() {
|
||||
moveToNext()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when the right side of the screen was clicked.
|
||||
* Moves a page to the left.
|
||||
*/
|
||||
protected open fun onRightSideTap() {
|
||||
moveToNext()
|
||||
override fun moveLeft() {
|
||||
moveToPrevious()
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves to the next page or requests the next chapter if it's the last one.
|
||||
*/
|
||||
override fun moveToNext() {
|
||||
protected fun moveToNext() {
|
||||
if (pager.currentItem != pager.adapter.count - 1) {
|
||||
pager.setCurrentItem(pager.currentItem + 1, transitions)
|
||||
} else {
|
||||
@ -285,7 +272,7 @@ abstract class PagerReader : BaseReader() {
|
||||
/**
|
||||
* Moves to the previous page or requests the previous chapter if it's the first one.
|
||||
*/
|
||||
override fun moveToPrevious() {
|
||||
protected fun moveToPrevious() {
|
||||
if (pager.currentItem != 0) {
|
||||
pager.setCurrentItem(pager.currentItem - 1, transitions)
|
||||
} else {
|
||||
|
@ -19,11 +19,31 @@ class RightToLeftReader : PagerReader() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLeftSideTap() {
|
||||
/**
|
||||
* Moves a page to the right.
|
||||
*/
|
||||
override fun moveRight() {
|
||||
moveToPrevious()
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves a page to the left.
|
||||
*/
|
||||
override fun moveLeft() {
|
||||
moveToNext()
|
||||
}
|
||||
|
||||
override fun onRightSideTap() {
|
||||
/**
|
||||
* Moves a page down.
|
||||
*/
|
||||
override fun moveDown() {
|
||||
moveToNext()
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves a page up.
|
||||
*/
|
||||
override fun moveUp() {
|
||||
moveToPrevious()
|
||||
}
|
||||
|
||||
|
@ -22,7 +22,7 @@ class WebtoonAdapter(val fragment: WebtoonReader) : RecyclerView.Adapter<Webtoon
|
||||
/**
|
||||
* Touch listener for images in holders.
|
||||
*/
|
||||
val touchListener = View.OnTouchListener { v, ev -> fragment.gestureDetector.onTouchEvent(ev) }
|
||||
val touchListener = View.OnTouchListener { v, ev -> fragment.imageGestureDetector.onTouchEvent(ev) }
|
||||
|
||||
/**
|
||||
* Returns the number of pages.
|
||||
|
@ -64,6 +64,7 @@ class WebtoonHolder(private val view: View, private val adapter: WebtoonAdapter)
|
||||
setBitmapDecoderClass(webtoonReader.bitmapDecoderClass)
|
||||
setVerticalScrollingParent(true)
|
||||
setOnTouchListener(adapter.touchListener)
|
||||
setOnLongClickListener { webtoonReader.onLongClick(page) }
|
||||
setOnImageEventListener(object : SubsamplingScaleImageView.DefaultOnImageEventListener() {
|
||||
override fun onReady() {
|
||||
onImageDecoded()
|
||||
|
@ -3,14 +3,11 @@ package eu.kanade.tachiyomi.ui.reader.viewer.webtoon
|
||||
import android.os.Bundle
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.view.*
|
||||
import android.view.GestureDetector.SimpleOnGestureListener
|
||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.source.model.Page
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderChapter
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.base.BaseReader
|
||||
import eu.kanade.tachiyomi.util.toast
|
||||
import eu.kanade.tachiyomi.widget.PreCachingLayoutManager
|
||||
import rx.subscriptions.CompositeSubscription
|
||||
|
||||
@ -55,9 +52,9 @@ class WebtoonReader : BaseReader() {
|
||||
private set
|
||||
|
||||
/**
|
||||
* Gesture detector for touch events.
|
||||
* Gesture detector for image touch events.
|
||||
*/
|
||||
val gestureDetector by lazy { createGestureDetector() }
|
||||
val imageGestureDetector by lazy { GestureDetector(context, ImageGestureListener()) }
|
||||
|
||||
/**
|
||||
* Subscriptions used while the view exists.
|
||||
@ -122,39 +119,24 @@ class WebtoonReader : BaseReader() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the gesture detector for the reader.
|
||||
*
|
||||
* @return a gesture detector.
|
||||
* Gesture detector for Subsampling Scale Image View.
|
||||
*/
|
||||
protected fun createGestureDetector(): GestureDetector {
|
||||
return GestureDetector(context, object : SimpleOnGestureListener() {
|
||||
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
|
||||
if (isAdded) {
|
||||
val positionX = e.x
|
||||
inner class ImageGestureListener : GestureDetector.SimpleOnGestureListener() {
|
||||
|
||||
if (positionX < recycler.width * LEFT_REGION) {
|
||||
if (tappingEnabled) moveToPrevious()
|
||||
} else if (positionX > recycler.width * RIGHT_REGION) {
|
||||
if (tappingEnabled) moveToNext()
|
||||
} else {
|
||||
readerActivity.toggleMenu()
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
|
||||
if (isAdded) {
|
||||
val positionX = e.x
|
||||
|
||||
override fun onLongPress(e: MotionEvent) {
|
||||
if (isAdded) {
|
||||
val child = recycler.findChildViewUnder(e.rawX, e.rawY)
|
||||
val position = recycler.getChildAdapterPosition(child)
|
||||
val page = adapter.pages?.getOrNull(position)
|
||||
if (page != null)
|
||||
readerActivity.onLongPress(page)
|
||||
else
|
||||
context.toast(getString(R.string.unknown_error))
|
||||
if (positionX < recycler.width * LEFT_REGION) {
|
||||
if (tappingEnabled) moveLeft()
|
||||
} else if (positionX > recycler.width * RIGHT_REGION) {
|
||||
if (tappingEnabled) moveRight()
|
||||
} else {
|
||||
readerActivity.toggleMenu()
|
||||
}
|
||||
}
|
||||
})
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -205,14 +187,14 @@ class WebtoonReader : BaseReader() {
|
||||
/**
|
||||
* Moves to the next page or requests the next chapter if it's the last one.
|
||||
*/
|
||||
override fun moveToNext() {
|
||||
override fun moveRight() {
|
||||
recycler.smoothScrollBy(0, scrollDistance)
|
||||
}
|
||||
|
||||
/**
|
||||
* Moves to the previous page or requests the previous chapter if it's the first one.
|
||||
*/
|
||||
override fun moveToPrevious() {
|
||||
override fun moveLeft() {
|
||||
recycler.smoothScrollBy(0, -scrollDistance)
|
||||
}
|
||||
|
||||
|
@ -43,43 +43,17 @@ class RecentChaptersPresenter : BasePresenter<RecentChaptersFragment>() {
|
||||
*/
|
||||
private var chapters: List<RecentChapter>? = null
|
||||
|
||||
/**
|
||||
* The id of the restartable.
|
||||
*/
|
||||
val GET_RECENT_CHAPTERS = 1
|
||||
|
||||
/**
|
||||
* The id of the restartable.
|
||||
*/
|
||||
val CHAPTER_STATUS_CHANGES = 2
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
|
||||
// Used to get recent chapters
|
||||
restartableLatestCache(GET_RECENT_CHAPTERS,
|
||||
{ getRecentChaptersObservable() },
|
||||
{ view, chapters ->
|
||||
// Update adapter to show recent manga's
|
||||
view.onNextRecentChapters(chapters)
|
||||
}
|
||||
)
|
||||
getRecentChaptersObservable()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeLatestCache(RecentChaptersFragment::onNextRecentChapters)
|
||||
|
||||
// Used to update download status
|
||||
restartableLatestCache(CHAPTER_STATUS_CHANGES,
|
||||
{ getChapterStatusObservable() },
|
||||
{ view, download ->
|
||||
// Set chapter status
|
||||
view.onChapterStatusChange(download)
|
||||
},
|
||||
{ view, error -> Timber.e(error) }
|
||||
)
|
||||
getChapterStatusObservable()
|
||||
.subscribeLatestCache(RecentChaptersFragment::onChapterStatusChange,
|
||||
{ view, error -> Timber.e(error) })
|
||||
|
||||
if (savedState == null) {
|
||||
// Start fetching recent chapters
|
||||
start(GET_RECENT_CHAPTERS)
|
||||
start(CHAPTER_STATUS_CHANGES)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -119,7 +93,6 @@ class RecentChaptersPresenter : BasePresenter<RecentChaptersFragment>() {
|
||||
}
|
||||
}
|
||||
}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
/**
|
||||
@ -156,14 +129,29 @@ class RecentChaptersPresenter : BasePresenter<RecentChaptersFragment>() {
|
||||
* @param chapters the list of chapter from the database.
|
||||
*/
|
||||
private fun setDownloadedChapters(chapters: List<RecentChapter>) {
|
||||
val cachedDirs = mutableMapOf<Long, UniFile?>()
|
||||
// Cached list of downloaded manga directories.
|
||||
val mangaDirectories = mutableMapOf<Int, Array<UniFile>>()
|
||||
|
||||
chapters.forEach { chapter ->
|
||||
// Cached list of downloaded chapter directories for a manga.
|
||||
val chapterDirectories = mutableMapOf<Long, Array<UniFile>>()
|
||||
|
||||
for (chapter in chapters) {
|
||||
val manga = chapter.manga
|
||||
val mangaDir = cachedDirs.getOrPut(manga.id!!)
|
||||
{ downloadManager.findMangaDir(sourceManager.get(manga.source)!!, manga) }
|
||||
val source = sourceManager.get(manga.source) ?: continue
|
||||
|
||||
if (mangaDir?.findFile(downloadManager.getChapterDirName(chapter)) != null) {
|
||||
val mangaDirs = mangaDirectories.getOrPut(source.id) {
|
||||
downloadManager.findSourceDir(source)?.listFiles() ?: emptyArray()
|
||||
}
|
||||
|
||||
val mangaDirName = downloadManager.getMangaDirName(manga)
|
||||
val mangaDir = mangaDirs.find { it.name == mangaDirName } ?: continue
|
||||
|
||||
val chapterDirs = chapterDirectories.getOrPut(manga.id!!) {
|
||||
mangaDir.listFiles() ?: emptyArray()
|
||||
}
|
||||
|
||||
val chapterDirName = downloadManager.getChapterDirName(chapter)
|
||||
if (chapterDirs.any { it.name == chapterDirName }) {
|
||||
chapter.status = Download.DOWNLOADED
|
||||
}
|
||||
}
|
||||
|
@ -78,6 +78,7 @@ class SettingsActivity : BaseActivity(),
|
||||
companion object {
|
||||
const val FLAG_THEME_CHANGED = 0x1
|
||||
const val FLAG_DATABASE_CLEARED = 0x2
|
||||
const val FLAG_LANG_CHANGED = 0x4
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.util.LocaleHelper
|
||||
import eu.kanade.tachiyomi.util.plusAssign
|
||||
import eu.kanade.tachiyomi.widget.preference.IntListPreference
|
||||
import eu.kanade.tachiyomi.widget.preference.LibraryColumnsDialog
|
||||
@ -17,6 +18,7 @@ import net.xpece.android.support.preference.MultiSelectListPreference
|
||||
import rx.Observable
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.*
|
||||
|
||||
class SettingsGeneralFragment : SettingsFragment(),
|
||||
PreferenceFragmentCompat.OnPreferenceDisplayDialogCallback {
|
||||
@ -44,6 +46,8 @@ class SettingsGeneralFragment : SettingsFragment(),
|
||||
|
||||
val categoryUpdate: MultiSelectListPreference by bindPref(R.string.pref_library_update_categories_key)
|
||||
|
||||
val langPreference: IntListPreference by bindPref(R.string.pref_language_key)
|
||||
|
||||
override fun onViewCreated(view: View, savedState: Bundle?) {
|
||||
super.onViewCreated(view, savedState)
|
||||
|
||||
@ -101,6 +105,15 @@ class SettingsGeneralFragment : SettingsFragment(),
|
||||
activity.recreate()
|
||||
true
|
||||
}
|
||||
|
||||
langPreference.setOnPreferenceChangeListener { preference, newValue ->
|
||||
(activity as SettingsActivity).parentFlags = SettingsActivity.FLAG_LANG_CHANGED
|
||||
LocaleHelper.setLocale(Locale(LocaleHelper.intToLangCode(newValue.toString().toInt())))
|
||||
LocaleHelper.updateCfg(activity.application, activity.baseContext.resources.configuration)
|
||||
activity.recreate()
|
||||
true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun onPreferenceDisplayDialog(p0: PreferenceFragmentCompat?, p: Preference): Boolean {
|
||||
|
@ -19,7 +19,7 @@ import java.util.*
|
||||
fun syncChaptersWithSource(db: DatabaseHelper,
|
||||
sourceChapters: List<Chapter>,
|
||||
manga: Manga,
|
||||
source: Source) : Pair<Int, Int> {
|
||||
source: Source) : Pair<List<Chapter>, List<Chapter>> {
|
||||
|
||||
// Chapters from db.
|
||||
val dbChapters = db.getChapters(manga).executeAsBlocking()
|
||||
@ -44,22 +44,19 @@ fun syncChaptersWithSource(db: DatabaseHelper,
|
||||
// Chapters from the db not in the source.
|
||||
val toDelete = dbChapters.filterNot { it in sourceChapters }
|
||||
|
||||
// Amount of chapters added and deleted.
|
||||
var added = 0
|
||||
var deleted = 0
|
||||
|
||||
// Amount of chapters readded (different url but the same chapter number).
|
||||
var readded = 0
|
||||
val readded = mutableListOf<Chapter>()
|
||||
|
||||
db.inTransaction {
|
||||
val deletedChapterNumbers = TreeSet<Float>()
|
||||
val deletedReadChapterNumbers = TreeSet<Float>()
|
||||
if (!toDelete.isEmpty()) {
|
||||
for (c in toDelete) {
|
||||
if (c.read) {
|
||||
deletedReadChapterNumbers.add(c.chapter_number)
|
||||
}
|
||||
deletedChapterNumbers.add(c.chapter_number)
|
||||
}
|
||||
deleted = db.deleteChapters(toDelete).executeAsBlocking().results().size
|
||||
db.deleteChapters(toDelete).executeAsBlocking()
|
||||
}
|
||||
|
||||
if (!toAdd.isEmpty()) {
|
||||
@ -73,14 +70,16 @@ fun syncChaptersWithSource(db: DatabaseHelper,
|
||||
// Try to mark already read chapters as read when the source deletes them
|
||||
if (c.isRecognizedNumber && c.chapter_number in deletedReadChapterNumbers) {
|
||||
c.read = true
|
||||
readded++
|
||||
}
|
||||
if (c.isRecognizedNumber && c.chapter_number in deletedChapterNumbers) {
|
||||
readded.add(c)
|
||||
}
|
||||
}
|
||||
added = db.insertChapters(toAdd).executeAsBlocking().numberOfInserts()
|
||||
db.insertChapters(toAdd).executeAsBlocking()
|
||||
}
|
||||
|
||||
// Fix order in source.
|
||||
db.fixChaptersSourceOrder(sourceChapters).executeAsBlocking()
|
||||
}
|
||||
return Pair(added - readded, deleted - readded)
|
||||
return Pair(toAdd.subtract(readded).toList(), toDelete.subtract(readded).toList())
|
||||
}
|
||||
|
47
app/src/main/java/eu/kanade/tachiyomi/util/LocaleHelper.kt
Normal file
47
app/src/main/java/eu/kanade/tachiyomi/util/LocaleHelper.kt
Normal file
@ -0,0 +1,47 @@
|
||||
package eu.kanade.tachiyomi.util
|
||||
|
||||
import android.app.Application
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import android.view.ContextThemeWrapper
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.util.Locale
|
||||
|
||||
object LocaleHelper {
|
||||
|
||||
private val preferences: PreferencesHelper by injectLazy()
|
||||
|
||||
private var pLocale = Locale(intToLangCode(preferences.lang()))
|
||||
|
||||
fun setLocale(locale: Locale) {
|
||||
pLocale = locale
|
||||
Locale.setDefault(pLocale)
|
||||
}
|
||||
|
||||
fun updateCfg(wrapper: ContextThemeWrapper) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
val config = Configuration()
|
||||
config.setLocale(pLocale)
|
||||
wrapper.applyOverrideConfiguration(config)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateCfg(app: Application, config: Configuration) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||
config.locale = pLocale
|
||||
app.baseContext.resources.updateConfiguration(config, app.baseContext.resources.displayMetrics)
|
||||
}
|
||||
}
|
||||
|
||||
fun intToLangCode(i: Int): String {
|
||||
return when(i) {
|
||||
1 -> "en"
|
||||
2 -> "es"
|
||||
3 -> "it"
|
||||
4 -> "pt"
|
||||
else -> "" // System Language
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,8 +1,13 @@
|
||||
package eu.kanade.tachiyomi.util
|
||||
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.subscriptions.CompositeSubscription
|
||||
|
||||
fun Subscription?.isNullOrUnsubscribed() = this == null || isUnsubscribed
|
||||
|
||||
operator fun CompositeSubscription.plusAssign(subscription: Subscription) = add(subscription)
|
||||
operator fun CompositeSubscription.plusAssign(subscription: Subscription) = add(subscription)
|
||||
|
||||
fun <T, U, R> Observable<T>.combineLatest(o2: Observable<U>, combineFn: (T, U) -> R): Observable<R> {
|
||||
return Observable.combineLatest(this, o2, combineFn)
|
||||
}
|
@ -0,0 +1,363 @@
|
||||
package eu.kanade.tachiyomi.widget
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.support.annotation.CallSuper
|
||||
import android.support.design.R
|
||||
import android.support.design.internal.ScrimInsetsFrameLayout
|
||||
import android.support.graphics.drawable.VectorDrawableCompat
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.support.v4.view.ViewCompat
|
||||
import android.support.v7.widget.LinearLayoutManager
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.support.v7.widget.TintTypedArray
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.CheckBox
|
||||
import android.widget.CheckedTextView
|
||||
import android.widget.RadioButton
|
||||
import android.widget.TextView
|
||||
import eu.kanade.tachiyomi.util.getResourceColor
|
||||
import eu.kanade.tachiyomi.util.inflate
|
||||
import eu.kanade.tachiyomi.R as TR
|
||||
|
||||
/**
|
||||
* An alternative implementation of [android.support.design.widget.NavigationView], without menu
|
||||
* inflation and allowing customizable items (multiple selections, custom views, etc).
|
||||
*/
|
||||
@Suppress("LeakingThis")
|
||||
@SuppressLint("PrivateResource")
|
||||
open class ExtendedNavigationView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0)
|
||||
: ScrimInsetsFrameLayout(context, attrs, defStyleAttr) {
|
||||
|
||||
/**
|
||||
* Max width of the navigation view.
|
||||
*/
|
||||
private var maxWidth: Int
|
||||
|
||||
/**
|
||||
* Recycler view containing all the items.
|
||||
*/
|
||||
protected val recycler = RecyclerView(context)
|
||||
|
||||
init {
|
||||
// Custom attributes
|
||||
val a = TintTypedArray.obtainStyledAttributes(context, attrs,
|
||||
R.styleable.NavigationView, defStyleAttr,
|
||||
R.style.Widget_Design_NavigationView)
|
||||
|
||||
ViewCompat.setBackground(
|
||||
this, a.getDrawable(R.styleable.NavigationView_android_background))
|
||||
|
||||
if (a.hasValue(R.styleable.NavigationView_elevation)) {
|
||||
ViewCompat.setElevation(this, a.getDimensionPixelSize(
|
||||
R.styleable.NavigationView_elevation, 0).toFloat())
|
||||
}
|
||||
|
||||
ViewCompat.setFitsSystemWindows(this,
|
||||
a.getBoolean(R.styleable.NavigationView_android_fitsSystemWindows, false))
|
||||
|
||||
maxWidth = a.getDimensionPixelSize(R.styleable.NavigationView_android_maxWidth, 0)
|
||||
|
||||
a.recycle()
|
||||
|
||||
recycler.layoutManager = LinearLayoutManager(context)
|
||||
addView(recycler)
|
||||
}
|
||||
|
||||
/**
|
||||
* Overriden to measure the width of the navigation view.
|
||||
*/
|
||||
override fun onMeasure(widthSpec: Int, heightSpec: Int) {
|
||||
val width = when (MeasureSpec.getMode(widthSpec)) {
|
||||
MeasureSpec.AT_MOST -> MeasureSpec.makeMeasureSpec(
|
||||
Math.min(MeasureSpec.getSize(widthSpec), maxWidth), MeasureSpec.EXACTLY)
|
||||
MeasureSpec.UNSPECIFIED -> MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.EXACTLY)
|
||||
else -> widthSpec
|
||||
}
|
||||
// Let super sort out the height
|
||||
super.onMeasure(width, heightSpec)
|
||||
}
|
||||
|
||||
/**
|
||||
* Every item of the nav view. Generic items must belong to this list, custom items could be
|
||||
* implemented by an abstract class. If more customization is needed in the future, this can be
|
||||
* changed to an interface instead of sealed class.
|
||||
*/
|
||||
sealed class Item {
|
||||
/**
|
||||
* A view separator.
|
||||
*/
|
||||
class Separator(val paddingTop: Int = 0, val paddingBottom: Int = 0) : Item()
|
||||
|
||||
/**
|
||||
* A header with a title.
|
||||
*/
|
||||
class Header(val resTitle: Int) : Item()
|
||||
|
||||
/**
|
||||
* A checkbox.
|
||||
*/
|
||||
open class Checkbox(val resTitle: Int, var checked: Boolean = false) : Item()
|
||||
|
||||
/**
|
||||
* A checkbox belonging to a group. The group must handle selections and restrictions.
|
||||
*/
|
||||
class CheckboxGroup(resTitle: Int, override val group: Group, checked: Boolean = false)
|
||||
: Checkbox(resTitle, checked), GroupedItem
|
||||
|
||||
/**
|
||||
* A radio belonging to a group (a sole radio makes no sense). The group must handle
|
||||
* selections and restrictions.
|
||||
*/
|
||||
class Radio(val resTitle: Int, override val group: Group, var checked: Boolean = false)
|
||||
: Item(), GroupedItem
|
||||
|
||||
/**
|
||||
* An item with which needs more than two states (selected/deselected).
|
||||
*/
|
||||
abstract class MultiState(val resTitle: Int, var state: Int = 0) : Item() {
|
||||
|
||||
/**
|
||||
* Returns the drawable associated to every possible each state.
|
||||
*/
|
||||
abstract fun getStateDrawable(context: Context): Drawable?
|
||||
|
||||
/**
|
||||
* Creates a vector tinted with the accent color.
|
||||
*
|
||||
* @param context any context.
|
||||
* @param resId the vector resource to load and tint
|
||||
*/
|
||||
fun tintVector(context: Context, resId: Int): Drawable {
|
||||
return VectorDrawableCompat.create(context.resources, resId, context.theme)!!.apply {
|
||||
setTint(context.theme.getResourceColor(TR.attr.colorAccent))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An item with which needs more than two states (selected/deselected) belonging to a group.
|
||||
* The group must handle selections and restrictions.
|
||||
*/
|
||||
abstract class MultiStateGroup(resTitle: Int, override val group: Group, state: Int = 0)
|
||||
: MultiState(resTitle, state), GroupedItem
|
||||
|
||||
/**
|
||||
* A multistate item for sorting lists (unselected, ascending, descending).
|
||||
*/
|
||||
class MultiSort(resId: Int, group: Group) : MultiStateGroup(resId, group) {
|
||||
|
||||
companion object {
|
||||
const val SORT_NONE = 0
|
||||
const val SORT_ASC = 1
|
||||
const val SORT_DESC = 2
|
||||
}
|
||||
|
||||
override fun getStateDrawable(context: Context): Drawable? {
|
||||
return when (state) {
|
||||
SORT_ASC -> tintVector(context, TR.drawable.ic_keyboard_arrow_up_black_32dp)
|
||||
SORT_DESC -> tintVector(context, TR.drawable.ic_keyboard_arrow_down_black_32dp)
|
||||
SORT_NONE -> ContextCompat.getDrawable(context, TR.drawable.empty_drawable_32dp)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for an item belonging to a group.
|
||||
*/
|
||||
interface GroupedItem {
|
||||
val group: Group
|
||||
}
|
||||
|
||||
/**
|
||||
* A group containing a list of items.
|
||||
*/
|
||||
interface Group {
|
||||
|
||||
/**
|
||||
* An optional header for the group, typically a [Item.Header].
|
||||
*/
|
||||
val header: Item?
|
||||
|
||||
/**
|
||||
* An optional footer for the group, typically a [Item.Separator].
|
||||
*/
|
||||
val footer: Item?
|
||||
|
||||
/**
|
||||
* The items of the group, excluding header and footer.
|
||||
*/
|
||||
val items: List<Item>
|
||||
|
||||
/**
|
||||
* Creates all the elements of this group. Implementations can override this method for more
|
||||
* customization.
|
||||
*/
|
||||
fun createItems() = (mutableListOf<Item>() + header + items + footer).filterNotNull()
|
||||
|
||||
/**
|
||||
* Called after creating the list of items. Implementations should load the current values
|
||||
* into the models.
|
||||
*/
|
||||
fun initModels()
|
||||
|
||||
/**
|
||||
* Called when an item of this group is clicked. The group is responsible for all the
|
||||
* selections of its items.
|
||||
*/
|
||||
fun onItemClicked(item: Item)
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Base view holder.
|
||||
*/
|
||||
abstract class Holder(view: View) : RecyclerView.ViewHolder(view)
|
||||
|
||||
/**
|
||||
* Separator view holder.
|
||||
*/
|
||||
class SeparatorHolder(parent: ViewGroup)
|
||||
: Holder(parent.inflate(R.layout.design_navigation_item_separator))
|
||||
|
||||
/**
|
||||
* Header view holder.
|
||||
*/
|
||||
class HeaderHolder(parent: ViewGroup)
|
||||
: Holder(parent.inflate(R.layout.design_navigation_item_subheader))
|
||||
|
||||
/**
|
||||
* Clickable view holder.
|
||||
*/
|
||||
abstract class ClickableHolder(view: View, listener: View.OnClickListener?) : Holder(view) {
|
||||
init {
|
||||
itemView.setOnClickListener(listener)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Radio view holder.
|
||||
*/
|
||||
class RadioHolder(parent: ViewGroup, listener: View.OnClickListener?)
|
||||
: ClickableHolder(parent.inflate(TR.layout.navigation_view_radio), listener) {
|
||||
|
||||
val radio = itemView.findViewById(TR.id.nav_view_item) as RadioButton
|
||||
}
|
||||
|
||||
/**
|
||||
* Checkbox view holder.
|
||||
*/
|
||||
class CheckboxHolder(parent: ViewGroup, listener: View.OnClickListener?)
|
||||
: ClickableHolder(parent.inflate(TR.layout.navigation_view_checkbox), listener) {
|
||||
|
||||
val check = itemView.findViewById(TR.id.nav_view_item) as CheckBox
|
||||
}
|
||||
|
||||
/**
|
||||
* Multi state view holder.
|
||||
*/
|
||||
class MultiStateHolder(parent: ViewGroup, listener: View.OnClickListener?)
|
||||
: ClickableHolder(parent.inflate(TR.layout.navigation_view_checkedtext), listener) {
|
||||
|
||||
val text = itemView.findViewById(TR.id.nav_view_item) as CheckedTextView
|
||||
}
|
||||
|
||||
/**
|
||||
* Base adapter for the navigation view. It knows how to create and render every subclass of
|
||||
* [Item].
|
||||
*/
|
||||
abstract inner class Adapter(private val items: List<Item>) : RecyclerView.Adapter<Holder>() {
|
||||
|
||||
private val onClick = View.OnClickListener {
|
||||
val pos = recycler.getChildAdapterPosition(it)
|
||||
val item = items[pos]
|
||||
onItemClicked(item)
|
||||
}
|
||||
|
||||
fun notifyItemChanged(item: Item) {
|
||||
val pos = items.indexOf(item)
|
||||
if (pos != -1) notifyItemChanged(pos)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return items.size
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
val item = items[position]
|
||||
return when (item) {
|
||||
is Item.Header -> VIEW_TYPE_HEADER
|
||||
is Item.Separator -> VIEW_TYPE_SEPARATOR
|
||||
is Item.Radio -> VIEW_TYPE_RADIO
|
||||
is Item.Checkbox -> VIEW_TYPE_CHECKBOX
|
||||
is Item.MultiState -> VIEW_TYPE_MULTISTATE
|
||||
}
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
|
||||
return when (viewType) {
|
||||
VIEW_TYPE_HEADER -> HeaderHolder(parent)
|
||||
VIEW_TYPE_SEPARATOR -> SeparatorHolder(parent)
|
||||
VIEW_TYPE_RADIO -> RadioHolder(parent, onClick)
|
||||
VIEW_TYPE_CHECKBOX -> CheckboxHolder(parent, onClick)
|
||||
VIEW_TYPE_MULTISTATE -> MultiStateHolder(parent, onClick)
|
||||
else -> throw Exception("Unknown view type")
|
||||
}
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override fun onBindViewHolder(holder: Holder, position: Int) {
|
||||
when (holder) {
|
||||
is HeaderHolder -> {
|
||||
val view = holder.itemView as TextView
|
||||
val item = items[position] as Item.Header
|
||||
view.setText(item.resTitle)
|
||||
}
|
||||
is SeparatorHolder -> {
|
||||
val view = holder.itemView
|
||||
val item = items[position] as Item.Separator
|
||||
view.setPadding(0, item.paddingTop, 0, item.paddingBottom)
|
||||
}
|
||||
is RadioHolder -> {
|
||||
val item = items[position] as Item.Radio
|
||||
holder.radio.setText(item.resTitle)
|
||||
holder.radio.isChecked = item.checked
|
||||
}
|
||||
is CheckboxHolder -> {
|
||||
val item = items[position] as Item.CheckboxGroup
|
||||
holder.check.setText(item.resTitle)
|
||||
holder.check.isChecked = item.checked
|
||||
}
|
||||
is MultiStateHolder -> {
|
||||
val item = items[position] as Item.MultiStateGroup
|
||||
val drawable = item.getStateDrawable(context)
|
||||
holder.text.setText(item.resTitle)
|
||||
holder.text.setCompoundDrawablesWithIntrinsicBounds(drawable, null, null, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun onItemClicked(item: Item)
|
||||
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val VIEW_TYPE_HEADER = 100
|
||||
private const val VIEW_TYPE_SEPARATOR = 101
|
||||
private const val VIEW_TYPE_RADIO = 102
|
||||
private const val VIEW_TYPE_CHECKBOX = 103
|
||||
private const val VIEW_TYPE_MULTISTATE = 104
|
||||
}
|
||||
|
||||
}
|
8
app/src/main/res/drawable/empty_drawable_32dp.xml
Normal file
8
app/src/main/res/drawable/empty_drawable_32dp.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<shape
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@android:color/transparent"/>
|
||||
<size
|
||||
android:width="32dp"
|
||||
android:height="32dp" />
|
||||
</shape>
|
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="32dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M7.41,7.84L12,12.42l4.59,-4.58L18,9.25l-6,6 -6,-6z"/>
|
||||
</vector>
|
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="32dp"
|
||||
android:height="32dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M7.41,15.41L12,10.83l4.59,4.58L18,14l-6,-6 -6,6z"/>
|
||||
</vector>
|
9
app/src/main/res/drawable/ic_sort_white_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_sort_white_24dp.xml
Normal file
@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M3,18h6v-2L3,16v2zM3,6v2h18L21,6L3,6zM3,13h12v-2L3,11v2z"/>
|
||||
</vector>
|
@ -57,7 +57,9 @@
|
||||
android:layout_gravity="bottom"
|
||||
android:gravity="center"
|
||||
android:background="?colorPrimary"
|
||||
android:orientation="horizontal">
|
||||
android:orientation="horizontal"
|
||||
android:focusable="false"
|
||||
android:descendantFocusability="blocksDescendants">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/left_chapter"
|
||||
|
@ -1,53 +1,43 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?android:attr/listPreferredItemHeightLarge"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:background="?attr/selectable_list_drawable"
|
||||
>
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/material_component_lists_single_line_with_avatar_height"
|
||||
android:background="?attr/selectable_list_drawable">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/image"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerInParent="true"
|
||||
android:layout_width="@dimen/material_component_lists_single_line_with_avatar_height"
|
||||
android:layout_height="@dimen/material_component_lists_single_line_with_avatar_height"
|
||||
android:clickable="true"
|
||||
android:layout_marginLeft="@dimen/margin_left"
|
||||
android:layout_marginStart="@dimen/margin_left"
|
||||
android:layout_marginRight="@dimen/margin_right"
|
||||
android:layout_marginEnd="@dimen/margin_right"/>
|
||||
|
||||
<android.support.v7.widget.AppCompatImageView
|
||||
android:id="@+id/reorder"
|
||||
android:layout_width="50dp"
|
||||
android:layout_height="50dp"
|
||||
android:layout_marginLeft="@dimen/margin_left"
|
||||
android:layout_marginStart="@dimen/margin_left"
|
||||
android:layout_marginRight="@dimen/margin_right"
|
||||
android:layout_marginEnd="@dimen/margin_right"
|
||||
android:scaleType="center"
|
||||
android:layout_centerInParent="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
app:srcCompat="@drawable/ic_reorder_grey_24dp"
|
||||
android:tint="?android:attr/textColorPrimary"/>
|
||||
android:paddingLeft="@dimen/material_component_lists_icon_left_padding"
|
||||
android:paddingStart="@dimen/material_component_lists_icon_left_padding"
|
||||
android:paddingRight="0dp"
|
||||
android:paddingEnd="0dp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_toRightOf="@id/image"
|
||||
android:layout_toEndOf="@id/image"
|
||||
android:layout_toLeftOf="@id/reorder"
|
||||
android:layout_toStartOf="@id/reorder"
|
||||
android:layout_centerInParent="true"
|
||||
android:layout_marginLeft="@dimen/material_component_lists_text_left_padding"
|
||||
android:layout_marginStart="@dimen/material_component_lists_text_left_padding"
|
||||
android:layout_marginRight="@dimen/material_component_lists_single_line_with_avatar_height"
|
||||
android:layout_marginEnd="@dimen/material_component_lists_single_line_with_avatar_height"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:textAppearance="@style/TextAppearance.Regular.SubHeading"
|
||||
tools:text="Title"/>
|
||||
|
||||
</RelativeLayout>
|
||||
<ImageView
|
||||
android:id="@+id/reorder"
|
||||
android:layout_width="@dimen/material_component_lists_single_line_with_avatar_height"
|
||||
android:layout_height="@dimen/material_component_lists_single_line_with_avatar_height"
|
||||
android:scaleType="center"
|
||||
android:layout_gravity="end"
|
||||
app:srcCompat="@drawable/ic_reorder_grey_24dp"
|
||||
android:tint="?android:attr/textColorPrimary"/>
|
||||
|
||||
</FrameLayout>
|
8
app/src/main/res/layout/library_drawer.xml
Normal file
8
app/src/main/res/layout/library_drawer.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<eu.kanade.tachiyomi.ui.library.LibraryNavigationView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/nav_view2"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="end"
|
||||
android:fitsSystemWindows="false" />
|
23
app/src/main/res/layout/navigation_view_checkbox.xml
Normal file
23
app/src/main/res/layout/navigation_view_checkbox.xml
Normal file
@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/listPreferredItemHeightSmall"
|
||||
android:paddingLeft="?attr/listPreferredItemPaddingLeft"
|
||||
android:paddingRight="?attr/listPreferredItemPaddingRight"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:focusable="true">
|
||||
|
||||
<CheckBox
|
||||
android:id="@+id/nav_view_item"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:paddingLeft="@dimen/material_component_lists_icon_left_padding"
|
||||
android:background="@android:color/transparent"
|
||||
android:gravity="center_vertical|start"
|
||||
android:maxLines="1"
|
||||
android:clickable="false"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body2" />
|
||||
|
||||
</LinearLayout>
|
21
app/src/main/res/layout/navigation_view_checkedtext.xml
Normal file
21
app/src/main/res/layout/navigation_view_checkedtext.xml
Normal file
@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/listPreferredItemHeightSmall"
|
||||
android:paddingLeft="?attr/listPreferredItemPaddingLeft"
|
||||
android:paddingRight="?attr/listPreferredItemPaddingRight"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:focusable="true">
|
||||
|
||||
<CheckedTextView
|
||||
android:id="@+id/nav_view_item"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:drawablePadding="@dimen/material_component_lists_icon_left_padding"
|
||||
android:gravity="center_vertical|start"
|
||||
android:maxLines="1"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body2" />
|
||||
|
||||
</LinearLayout>
|
23
app/src/main/res/layout/navigation_view_radio.xml
Normal file
23
app/src/main/res/layout/navigation_view_radio.xml
Normal file
@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/listPreferredItemHeightSmall"
|
||||
android:paddingLeft="?attr/listPreferredItemPaddingLeft"
|
||||
android:paddingRight="?attr/listPreferredItemPaddingRight"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:focusable="true">
|
||||
|
||||
<RadioButton
|
||||
android:id="@+id/nav_view_item"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:paddingLeft="@dimen/material_component_lists_icon_left_padding"
|
||||
android:background="@android:color/transparent"
|
||||
android:gravity="center_vertical|start"
|
||||
android:maxLines="1"
|
||||
android:clickable="false"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body2" />
|
||||
|
||||
</LinearLayout>
|
@ -6,6 +6,11 @@
|
||||
android:title="@string/action_download"
|
||||
android:visible="true" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_delete"
|
||||
android:title="@string/action_delete"
|
||||
android:visible="false"/>
|
||||
|
||||
<item android:id="@+id/action_bookmark"
|
||||
android:title="@string/action_bookmark"
|
||||
android:visible="true" />
|
||||
@ -14,10 +19,6 @@
|
||||
android:title="@string/action_remove_bookmark"
|
||||
android:visible="true" />
|
||||
|
||||
<item android:id="@+id/action_delete"
|
||||
android:title="@string/action_delete"
|
||||
android:visible="false" />
|
||||
|
||||
<item android:id="@+id/action_mark_as_read"
|
||||
android:title="@string/action_mark_as_read" />
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
<menu 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" tools:context=".MainActivity">
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:context=".MainActivity">
|
||||
|
||||
<item
|
||||
android:id="@+id/action_search"
|
||||
@ -13,36 +14,17 @@
|
||||
android:id="@+id/action_filter"
|
||||
android:icon="@drawable/ic_filter_list_white_24dp"
|
||||
android:title="@string/action_filter"
|
||||
app:showAsAction="ifRoom">
|
||||
<menu>
|
||||
<item
|
||||
android:id="@+id/action_filter_downloaded"
|
||||
android:checkable="true"
|
||||
android:title="@string/action_filter_downloaded"/>
|
||||
<item
|
||||
android:id="@+id/action_filter_unread"
|
||||
android:checkable="true"
|
||||
android:title="@string/action_filter_unread"/>
|
||||
<item
|
||||
android:id="@+id/action_filter_empty"
|
||||
android:title="@string/action_filter_empty"/>
|
||||
</menu>
|
||||
</item>
|
||||
app:showAsAction="ifRoom"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/action_update_library"
|
||||
android:icon="@drawable/ic_refresh_white_24dp"
|
||||
android:title="@string/action_update_library"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/action_library_display_mode"
|
||||
android:title="@string/action_display_mode"
|
||||
app:showAsAction="never"/>
|
||||
app:showAsAction="ifRoom"/>
|
||||
|
||||
<item
|
||||
android:id="@+id/action_edit_categories"
|
||||
android:title="@string/action_edit_categories"
|
||||
app:showAsAction="never" />
|
||||
app:showAsAction="never"/>
|
||||
|
||||
</menu>
|
||||
|
@ -1,6 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<changelog bulletedList="true">
|
||||
|
||||
<changelogversion versionName="v0.4.1" changeDate="">
|
||||
<changelogtext>Added an app's language selector.</changelogtext>
|
||||
|
||||
<changelogtext>Added options to sort the library and merged them with the filters.</changelogtext>
|
||||
|
||||
<changelogtext>Added an option to automatically download chapters.</changelogtext>
|
||||
|
||||
<changelogtext>Fixed performance issues when using a custom downloads directory, especially in the library updates tab.</changelogtext>
|
||||
|
||||
<changelogtext>Fixed gesture conflicts with the contextual menu and the webtoon reader.</changelogtext>
|
||||
|
||||
<changelogtext>Fixed wrong page direction when using volume keys for the right to left reader.</changelogtext>
|
||||
|
||||
<changelogtext>Fixed many crashes.</changelogtext>
|
||||
</changelogversion>
|
||||
|
||||
<changelogversion versionName="v0.4.0" changeDate="">
|
||||
<changelogtext>The download manager has been rewritten and it's possible some of your downloads
|
||||
aren't recognized anymore. It's recommended to manually delete everything and start over.
|
||||
|
@ -188,4 +188,20 @@
|
||||
<item>2</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="languages">
|
||||
<item>@string/system_default</item>
|
||||
<item>@string/english</item>
|
||||
<item>@string/spanish</item>
|
||||
<item>@string/italian</item>
|
||||
<item>@string/portuguese</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="languages_values">
|
||||
<item>0</item>
|
||||
<item>1</item>
|
||||
<item>2</item>
|
||||
<item>3</item>
|
||||
<item>4</item>
|
||||
</string-array>
|
||||
|
||||
</resources>
|
@ -21,6 +21,7 @@
|
||||
<string name="pref_theme_key">pref_theme_key</string>
|
||||
<string name="pref_library_update_restriction_key">library_update_restriction</string>
|
||||
<string name="pref_start_screen_key">start_screen</string>
|
||||
<string name="pref_language_key">language</string>
|
||||
|
||||
<string name="pref_default_viewer_key">pref_default_viewer_key</string>
|
||||
<string name="pref_image_scale_type_key">pref_image_scale_type_key</string>
|
||||
@ -41,6 +42,7 @@
|
||||
<string name="pref_read_with_tapping_key">reader_tap</string>
|
||||
<string name="pref_filter_downloaded_key">pref_filter_downloaded_key</string>
|
||||
<string name="pref_filter_unread_key">pref_filter_unread_key</string>
|
||||
<string name="pref_library_sorting_mode_key">library_sorting_mode</string>
|
||||
|
||||
<string name="pref_download_directory_key">download_directory</string>
|
||||
<string name="pref_download_slots_key">pref_download_slots_key</string>
|
||||
@ -64,6 +66,8 @@
|
||||
<string name="pref_display_catalogue_as_list">pref_display_catalogue_as_list</string>
|
||||
<string name="pref_last_catalogue_source_key">pref_last_catalogue_source_key</string>
|
||||
|
||||
<string name="pref_download_new_key">download_new</string>
|
||||
|
||||
<!-- String Fonts -->
|
||||
<string name="font_roboto_medium">sans-serif</string>
|
||||
<string name="font_roboto_regular">sans-serif</string>
|
||||
|
@ -23,6 +23,9 @@
|
||||
<string name="action_filter_unread">Unread</string>
|
||||
<string name="action_filter_read">Read</string>
|
||||
<string name="action_filter_empty">Remove filter</string>
|
||||
<string name="action_sort_alpha">Alphabetically</string>
|
||||
<string name="action_sort_last_read">Last read</string>
|
||||
<string name="action_sort_last_updated">Last updated</string>
|
||||
<string name="action_search">Search</string>
|
||||
<string name="action_select_all">Select all</string>
|
||||
<string name="action_mark_as_read">Mark as read</string>
|
||||
@ -57,6 +60,9 @@
|
||||
<string name="action_open_in_browser">Open in browser</string>
|
||||
<string name="action_add_to_home_screen">Add to home screen</string>
|
||||
<string name="action_display_mode">Change display mode</string>
|
||||
<string name="action_display">Display</string>
|
||||
<string name="action_display_grid">Grid</string>
|
||||
<string name="action_display_list">List</string>
|
||||
<string name="action_set_filter">Set filter</string>
|
||||
<string name="action_cancel">Cancel</string>
|
||||
<string name="action_sort">Sort</string>
|
||||
@ -105,6 +111,14 @@
|
||||
<string name="light_theme">Main theme</string>
|
||||
<string name="dark_theme">Dark theme</string>
|
||||
<string name="pref_start_screen">Start screen</string>
|
||||
<string name="pref_language">Language</string>
|
||||
|
||||
<!-- Languages -->
|
||||
<string name="system_default">System Default</string>
|
||||
<string name="english">English</string>
|
||||
<string name="spanish">Spanish</string>
|
||||
<string name="italian">Italian</string>
|
||||
<string name="portuguese">Portuguese</string>
|
||||
|
||||
<!-- Reader section -->
|
||||
<string name="pref_fullscreen">Fullscreen</string>
|
||||
@ -163,6 +177,7 @@
|
||||
<string name="third_to_last">Third to last chapter</string>
|
||||
<string name="fourth_to_last">Fourth to last chapter</string>
|
||||
<string name="fifth_to_last">Fifth to last chapter</string>
|
||||
<string name="pref_download_new">Download new chapters</string>
|
||||
|
||||
<!-- Sync section -->
|
||||
<string name="services">Services</string>
|
||||
@ -211,6 +226,9 @@
|
||||
<string name="select_source">Select a source</string>
|
||||
<string name="no_valid_sources">Please enable at least one valid source</string>
|
||||
|
||||
<!-- Manga activity -->
|
||||
<string name="manga_not_in_db">This manga was removed from the database!</string>
|
||||
|
||||
<!-- Manga info fragment -->
|
||||
<string name="manga_detail_tab">Info</string>
|
||||
<string name="description">Description</string>
|
||||
@ -293,6 +311,7 @@
|
||||
<string name="no_previous_chapter">Previous chapter not found</string>
|
||||
<string name="decode_image_error">Image could not be loaded.\nTry changing the image decoder or with one of the options below</string>
|
||||
<string name="confirm_update_manga_sync">Update last chapter read in enabled services to %1$d?</string>
|
||||
<string name="confirm_set_image_as_cover">Do you want to set this image as the cover?</string>
|
||||
<string name="viewer_for_this_series">Viewer for this series</string>
|
||||
|
||||
<!-- Backup fragment -->
|
||||
|
@ -44,6 +44,15 @@
|
||||
android:summary="%s"
|
||||
android:title="@string/pref_remove_after_read" />
|
||||
|
||||
<PreferenceCategory
|
||||
android:persistent="false"
|
||||
android:title="@string/pref_download_new" />
|
||||
|
||||
<SwitchPreference
|
||||
android:defaultValue="false"
|
||||
android:key="@string/pref_download_new_key"
|
||||
android:title="@string/pref_download_new"/>
|
||||
|
||||
</PreferenceScreen>
|
||||
|
||||
</PreferenceScreen>
|
@ -10,6 +10,14 @@
|
||||
android:title="@string/pref_category_general"
|
||||
app:asp_tintEnabled="true">
|
||||
|
||||
<eu.kanade.tachiyomi.widget.preference.IntListPreference
|
||||
android:defaultValue="0"
|
||||
android:entries="@array/languages"
|
||||
android:entryValues="@array/languages_values"
|
||||
android:key="@string/pref_language_key"
|
||||
android:summary="%s"
|
||||
android:title="@string/pref_language" />
|
||||
|
||||
<eu.kanade.tachiyomi.widget.preference.IntListPreference
|
||||
android:defaultValue="1"
|
||||
android:entries="@array/themes"
|
||||
|
@ -6,7 +6,7 @@ buildscript {
|
||||
jcenter()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:2.2.2'
|
||||
classpath 'com.android.tools.build:gradle:2.2.3'
|
||||
classpath 'com.github.ben-manes:gradle-versions-plugin:0.13.0'
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
|
Reference in New Issue
Block a user