Compare commits

...

24 Commits

Author SHA1 Message Date
len
fd76255cf6 Release 0.4.1 2016-12-18 21:05:33 +01:00
len
d180631877 Add ripple effect to filter nav view 2016-12-18 20:29:46 +01:00
len
1977e21363 Fix method conflicts 2016-12-18 16:59:06 +01:00
len
e1a3ee1b81 Bugfixes 2016-12-18 16:35:39 +01:00
cc43d9daed fixes wrong getBroadcast calls from imageNotification (#585) 2016-12-18 15:15:44 +01:00
len
79705df499 Apply material design guidelines to categories 2016-12-18 13:08:56 +01:00
len
36bbb906c1 Library sort change doesn't trigger filtering 2016-12-15 18:51:12 +01:00
len
816cc17ed3 Fix #577. Fix language not applied in reader activity. 2016-12-14 22:33:24 +01:00
len
97e3b5d2ab Add unread sorting 2016-12-13 22:23:49 +01:00
79ab9d80f2 Improved last_read sorting (#576) 2016-12-13 21:36:26 +01:00
len
32511149d1 Format fixes. Move lang setting to the first entry (looks better IMO) 2016-12-13 21:07:48 +01:00
cc9fd53abb Implement language switcher (#563)
* Implement language switching using BaseActivity

* Add requested changes

* Cleanup App.kt Imports and add pref_language_key

* Acutally use @string for key

* Use string resource for language preference title
2016-12-13 20:47:46 +01:00
len
4061c7450b Better network error handling 2016-12-12 20:53:44 +01:00
len
9ad535bde6 Optimize library downloaded filter 2016-12-11 23:59:25 +01:00
b067096fc7 Add drawer to filter and sort the library (#570)
* Add additional drawer to filter and sort the library

* Tint icon when there's a filter active

* Comments and minor changes
2016-12-11 12:43:44 +01:00
len
2dd58e5f7d Ask for confirmation before changing the cover. Fixes #562 2016-12-10 23:16:46 +01:00
len
7c42ab885b Readers know how to move to each side. Fix #566 2016-12-10 14:49:56 +01:00
len
26b283d44d Fix webtoon reader touch events. #561 2016-12-10 14:01:16 +01:00
len
8c1b07c4ba Handle null directories as empty arrays 2016-12-10 12:22:44 +01:00
len
f98e0858a7 Improve download discovery performance in library updates view 2016-12-09 20:23:48 +01:00
8b60d5bfcb Add optional to automatically download new chapers (#538)
* Add optional to automatically download new chapers

* Only trigger download once
2016-12-06 17:22:03 +01:00
len
30b4c6e755 Remove some state from the library view 2016-12-04 23:58:46 +01:00
len
3d2a98451b Avoid going to db when a library filter is changed 2016-12-04 23:48:29 +01:00
aba528b227 Added option to sort library (#536)
* Initial code

* Added all sort options

* Fixes

* Removed sort by added. Some renaming

* Removed date added database calls

* Fixes
2016-12-04 20:22:12 +01:00
67 changed files with 1548 additions and 514 deletions

View File

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

View File

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

View File

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

View File

@ -40,7 +40,6 @@ interface HistoryQueries : DbProvider {
.build())
.prepare()
/**
* Updates the history last read.
* Inserts history object if not yet in database

View File

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

View File

@ -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.
*/

View File

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

View File

@ -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.
*

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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].

View File

@ -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.

View File

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

View File

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

View File

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

View File

@ -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()
}
/**

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 {

View File

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

View File

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

View File

@ -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 {

View File

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

View File

@ -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.

View File

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

View File

@ -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 {

View File

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

View File

@ -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.

View File

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

View File

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

View File

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

View File

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

View File

@ -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 {

View File

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

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

View File

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

View File

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

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

View File

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

View File

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

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

View File

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

View File

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

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

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

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

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

View File

@ -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" />

View File

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

View File

@ -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.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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