diff --git a/app/src/main/java/eu/kanade/tachiyomi/Constants.kt b/app/src/main/java/eu/kanade/tachiyomi/Constants.kt index c6e432482..fcfd4b56c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/Constants.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/Constants.kt @@ -6,4 +6,8 @@ object Constants { const val NOTIFICATION_DOWNLOAD_CHAPTER_ID = 3 const val NOTIFICATION_DOWNLOAD_CHAPTER_ERROR_ID = 4 const val NOTIFICATION_DOWNLOAD_IMAGE_ID = 5 + + const val SORT_LIBRARY_ALPHA = 0 + const val SORT_LIBRARY_LAST_READ = 1 + const val SORT_LIBRARY_LAST_UPDATED = 2 } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/HistoryQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/HistoryQueries.kt index ec404bad2..0d2e0f611 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/HistoryQueries.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/HistoryQueries.kt @@ -40,6 +40,15 @@ interface HistoryQueries : DbProvider { .build()) .prepare() + fun getLastHistoryByMangaId(mangaId: Long) = db.get() + .`object`(History::class.java) + .withQuery(RawQuery.builder() + .query(getLastHistoryByMangaId()) + .args(mangaId) + .observesTables(HistoryTable.TABLE) + .build()) + .prepare() + /** * Updates the history last read. diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt index 1b058ac3f..a3d6be9f8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/MangaQueries.kt @@ -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) = db.delete().objects(mangas).prepare() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt index 59b0d0a9a..46bc39759 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt @@ -73,6 +73,19 @@ fun getHistoryByMangaId() = """ WHERE ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = ? AND ${History.TABLE}.${History.COL_CHAPTER_ID} = ${Chapter.TABLE}.${Chapter.COL_ID} """ +fun getLastHistoryByMangaId() = """ + SELECT ${History.TABLE}.* + FROM ${History.TABLE} + JOIN ${Chapter.TABLE} + ON ${History.TABLE}.${History.COL_CHAPTER_ID} = ${Chapter.TABLE}.${Chapter.COL_ID} + LEFT JOIN ( + SELECT MAX(${History.TABLE}.${History.COL_LAST_READ}) AS max + FROM ${History.TABLE} + GROUP BY ${History.COL_LAST_READ} + ) AS M + WHERE ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = ? AND M.max = ${History.TABLE}.${History.COL_LAST_READ} +""" + /** * Query to get the categories for a manga. */ diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaLastUpdatedPutResolver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaLastUpdatedPutResolver.kt new file mode 100644 index 000000000..8b2672ea9 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaLastUpdatedPutResolver.kt @@ -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() { + + 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) + } + +} + diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt index 928ca45bb..cbe8e7d7b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt @@ -251,7 +251,13 @@ class LibraryUpdateService : Service() { .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()) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt index 9e853d1e4..0aff5895b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt @@ -83,6 +83,8 @@ 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) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt index 97a66fe14..48b866464 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt @@ -126,6 +126,8 @@ class PreferencesHelper(context: Context) { fun filterUnread() = rxPrefs.getBoolean(keys.filterUnread, false) + fun librarySortingMode() = rxPrefs.getInteger(keys.librarySortingMode, 0) + fun automaticUpdates() = prefs.getBoolean(keys.automaticUpdates, false) fun hiddenCatalogues() = rxPrefs.getStringSet("hidden_catalogues", emptySet()) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt index f3c5e0a0b..df0d3ebbd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CataloguePresenter.kt @@ -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]. diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryFragment.kt index 7e3a0b945..b873f5f9b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryFragment.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryFragment.kt @@ -11,6 +11,7 @@ import android.support.v7.widget.SearchView import android.view.* import com.afollestad.materialdialogs.MaterialDialog import com.f2prateek.rx.preferences.Preference +import eu.kanade.tachiyomi.Constants import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Manga @@ -83,6 +84,11 @@ class LibraryFragment : BaseRxFragment(), ActionMode.Callback */ var isFilterUnread = false + /** + * Sorting mode for library + */ + var sortingMode = 0 + /** * Number of manga per row in grid mode. */ @@ -123,8 +129,9 @@ class LibraryFragment : BaseRxFragment(), ActionMode.Callback override fun onCreate(savedState: Bundle?) { super.onCreate(savedState) setHasOptionsMenu(true) - isFilterDownloaded = preferences.filterDownloaded().get() as Boolean - isFilterUnread = preferences.filterUnread().get() as Boolean + isFilterDownloaded = preferences.filterDownloaded().getOrDefault() + isFilterUnread = preferences.filterUnread().getOrDefault() + sortingMode = preferences.librarySortingMode().getOrDefault() } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View? { @@ -179,12 +186,37 @@ class LibraryFragment : BaseRxFragment(), ActionMode.Callback super.onSaveInstanceState(outState) } - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - inflater.inflate(R.menu.library, menu) - + /** + * Prepare the Fragment host's standard options menu to be displayed. This is + * called right before the menu is shown, every time it is shown. You can + * use this method to efficiently enable/disable items or otherwise + * dynamically modify the contents. + * + * @param menu The options menu as last shown or first initialized by + */ + override fun onPrepareOptionsMenu(menu: Menu) { // Initialize search menu val filterDownloadedItem = menu.findItem(R.id.action_filter_downloaded) val filterUnreadItem = menu.findItem(R.id.action_filter_unread) + val sortModeAlpha = menu.findItem(R.id.action_sort_alpha) + val sortModeLastRead = menu.findItem(R.id.action_sort_last_read) + val sortModeLastUpdated = menu.findItem(R.id.action_sort_last_updated) + + // Set correct checkbox filter + filterDownloadedItem.isChecked = isFilterDownloaded + filterUnreadItem.isChecked = isFilterUnread + + // Set correct radio button sort + when (sortingMode) { + Constants.SORT_LIBRARY_ALPHA -> sortModeAlpha.isChecked = true + Constants.SORT_LIBRARY_LAST_READ -> sortModeLastRead.isChecked = true + Constants.SORT_LIBRARY_LAST_UPDATED -> sortModeLastUpdated.isChecked = true + } + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.library, menu) + val searchItem = menu.findItem(R.id.action_search) val searchView = searchItem.actionView as SearchView @@ -194,9 +226,6 @@ class LibraryFragment : BaseRxFragment(), ActionMode.Callback searchView.clearFocus() } - filterDownloadedItem.isChecked = isFilterDownloaded - filterUnreadItem.isChecked = isFilterUnread - searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String): Boolean { onSearchTextChange(query) @@ -219,7 +248,7 @@ class LibraryFragment : BaseRxFragment(), ActionMode.Callback // Update settings. preferences.filterUnread().set(isFilterUnread) // Apply filter. - onFilterCheckboxChanged() + onFilterOrSortChanged() } R.id.action_filter_downloaded -> { // Change downloaded filter status. @@ -227,7 +256,7 @@ class LibraryFragment : BaseRxFragment(), ActionMode.Callback // Update settings. preferences.filterDownloaded().set(isFilterDownloaded) // Apply filter. - onFilterCheckboxChanged() + onFilterOrSortChanged() } R.id.action_filter_empty -> { // Remove filter status. @@ -237,7 +266,22 @@ class LibraryFragment : BaseRxFragment(), ActionMode.Callback preferences.filterUnread().set(isFilterUnread) preferences.filterDownloaded().set(isFilterDownloaded) // Apply filter - onFilterCheckboxChanged() + onFilterOrSortChanged() + } + R.id.action_sort_alpha -> { + sortingMode = Constants.SORT_LIBRARY_ALPHA + preferences.librarySortingMode().set(sortingMode) + onFilterOrSortChanged() + } + R.id.action_sort_last_read -> { + sortingMode = Constants.SORT_LIBRARY_LAST_READ + preferences.librarySortingMode().set(sortingMode) + onFilterOrSortChanged() + } + R.id.action_sort_last_updated -> { + sortingMode = Constants.SORT_LIBRARY_LAST_UPDATED + preferences.librarySortingMode().set(sortingMode) + onFilterOrSortChanged() } R.id.action_library_display_mode -> swapDisplayMode() R.id.action_update_library -> { @@ -256,7 +300,7 @@ class LibraryFragment : BaseRxFragment(), ActionMode.Callback /** * Applies filter change */ - private fun onFilterCheckboxChanged() { + private fun onFilterOrSortChanged() { presenter.resubscribeLibrary() activity.supportInvalidateOptionsMenu() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt index 1aba27e3d..7b7e39082 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt @@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.library import android.os.Bundle import android.util.Pair +import eu.kanade.tachiyomi.Constants import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.Category @@ -133,10 +134,12 @@ class LibraryPresenter : BasePresenter() { */ fun getLibraryMangasObservable(): Observable>> { return db.getLibraryMangas().asRxObservable() - .flatMap { mangas -> - Observable.from(mangas) + .flatMap { + Observable.from(it) // Filter library by options .filter { filterManga(it) } + .toSortedList { manga1, manga2 -> sortManga(manga1, manga2) } + .flatMap { Observable.from(it) } .groupBy { it.category } .flatMap { group -> group.toList().map { Pair(group.key, it) } } .toMap({ it.first }, { it.second }) @@ -159,6 +162,33 @@ class LibraryPresenter : BasePresenter() { start(GET_LIBRARY) } + /** + * Compares the two manga determined by sorting mode. + * Returns zero if this object is equal to the specified other object, + * a negative number if it's less than other, or a positive number if it's greater than other. + * + * @param manga1 first manga to compare + * @param manga2 second manga to compare + */ + fun sortManga(manga1: Manga, manga2: Manga): Int { + when (preferences.librarySortingMode().getOrDefault()) { + Constants.SORT_LIBRARY_ALPHA -> return manga1.title.compareTo(manga2.title) + Constants.SORT_LIBRARY_LAST_READ -> { + var a = 0L + var b = 0L + manga1.id?.let { manga1Id -> + manga2.id?.let { manga2Id -> + db.getLastHistoryByMangaId(manga1Id).executeAsBlocking()?.let { a = it.last_read } + db.getLastHistoryByMangaId(manga2Id).executeAsBlocking()?.let { b = it.last_read } + } + } + return b.compareTo(a) + } + Constants.SORT_LIBRARY_LAST_UPDATED -> return manga2.last_update.compareTo(manga1.last_update) + else -> return manga1.title.compareTo(manga2.title) + } + } + /** * Filters an entry of the library. * diff --git a/app/src/main/res/drawable/ic_sort_white_24dp.xml b/app/src/main/res/drawable/ic_sort_white_24dp.xml new file mode 100644 index 000000000..a0c153ad0 --- /dev/null +++ b/app/src/main/res/drawable/ic_sort_white_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/menu/library.xml b/app/src/main/res/menu/library.xml index 27fabce9b..e128d8769 100644 --- a/app/src/main/res/menu/library.xml +++ b/app/src/main/res/menu/library.xml @@ -1,6 +1,7 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + tools:context=".MainActivity"> + + + + + + + + + + + app:showAsAction="ifRoom"/> + app:showAsAction="never"/> diff --git a/app/src/main/res/raw/changelog_debug.xml b/app/src/main/res/raw/changelog_debug.xml index 2393bc919..81999d770 100644 --- a/app/src/main/res/raw/changelog_debug.xml +++ b/app/src/main/res/raw/changelog_debug.xml @@ -28,4 +28,12 @@ + + Library sort for "last updated" will only work with manga updated after this + version. + + + \ No newline at end of file diff --git a/app/src/main/res/values/keys.xml b/app/src/main/res/values/keys.xml index 626343711..42a9c84b2 100644 --- a/app/src/main/res/values/keys.xml +++ b/app/src/main/res/values/keys.xml @@ -41,6 +41,7 @@ reader_tap pref_filter_downloaded_key pref_filter_unread_key + library_sorting_mode download_directory pref_download_slots_key diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ec71d04fc..03866d5c3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -23,6 +23,9 @@ Unread Read Remove filter + Alphabetically + Last read + Last updated Search Select all Mark as read