浏览代码

Added recently read tab (#316)

Bram van de Kerkhof 8 年之前
父节点
当前提交
7ba898f701
共有 38 个文件被更改,包括 932 次插入38 次删除
  1. 2 1
      app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseHelper.kt
  2. 9 2
      app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenHelper.kt
  3. 58 0
      app/src/main/java/eu/kanade/tachiyomi/data/database/models/History.java
  4. 27 0
      app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaChapterHistory.java
  5. 53 0
      app/src/main/java/eu/kanade/tachiyomi/data/database/queries/HistoryQueries.kt
  6. 33 0
      app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt
  7. 52 0
      app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/HistoryLastReadPutResolver.kt
  8. 1 1
      app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaChapterGetResolver.kt
  9. 51 0
      app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaChapterHistoryGetResolver.kt
  10. 48 0
      app/src/main/java/eu/kanade/tachiyomi/data/database/tables/HistoryTable.kt
  11. 3 1
      app/src/main/java/eu/kanade/tachiyomi/injection/component/AppComponent.kt
  12. 3 1
      app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt
  13. 9 0
      app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt
  14. 2 2
      app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersAdapter.kt
  15. 2 2
      app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersFragment.kt
  16. 4 4
      app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersHolder.kt
  17. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersPresenter.kt
  18. 1 1
      app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/SectionViewHolder.kt
  19. 52 0
      app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadAdapter.kt
  20. 129 0
      app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadFragment.kt
  21. 96 0
      app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadHolder.kt
  22. 97 0
      app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadPresenter.kt
  23. 9 0
      app/src/main/res/drawable/ic_glasses_black_128dp.xml
  24. 9 0
      app/src/main/res/drawable/ic_glasses_black_24dp.xml
  25. 0 9
      app/src/main/res/drawable/ic_history_black_128dp.xml
  26. 0 9
      app/src/main/res/drawable/ic_history_black_24dp.xml
  27. 9 0
      app/src/main/res/drawable/ic_update_black_128dp.xml
  28. 9 0
      app/src/main/res/drawable/ic_update_black_24dp.xml
  29. 22 0
      app/src/main/res/layout/dialog_remove_recently.xml
  30. 3 1
      app/src/main/res/layout/fragment_recent_chapters.xml
  31. 16 0
      app/src/main/res/layout/fragment_recent_manga.xml
  32. 0 0
      app/src/main/res/layout/item_recent_chapters.xml
  33. 82 0
      app/src/main/res/layout/item_recent_manga.xml
  34. 6 1
      app/src/main/res/menu/menu_navigation.xml
  35. 8 0
      app/src/main/res/values/arrays.xml
  36. 2 0
      app/src/main/res/values/colors.xml
  37. 14 2
      app/src/main/res/values/strings.xml
  38. 10 0
      app/src/main/res/values/styles.xml

+ 2 - 1
app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseHelper.kt

@@ -9,7 +9,7 @@ import eu.kanade.tachiyomi.data.database.queries.*
  * This class provides operations to manage the database through its interfaces.
  */
 open class DatabaseHelper(context: Context)
-: MangaQueries, ChapterQueries, MangaSyncQueries, CategoryQueries, MangaCategoryQueries {
+: MangaQueries, ChapterQueries, MangaSyncQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries {
 
     override val db = DefaultStorIOSQLite.builder()
             .sqliteOpenHelper(DbOpenHelper(context))
@@ -18,6 +18,7 @@ open class DatabaseHelper(context: Context)
             .addTypeMapping(MangaSync::class.java, MangaSyncSQLiteTypeMapping())
             .addTypeMapping(Category::class.java, CategorySQLiteTypeMapping())
             .addTypeMapping(MangaCategory::class.java, MangaCategorySQLiteTypeMapping())
+            .addTypeMapping(History::class.java, HistorySQLiteTypeMapping())
             .build()
 
     inline fun inTransaction(block: () -> Unit) = db.inTransaction(block)

+ 9 - 2
app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenHelper.kt

@@ -17,7 +17,7 @@ class DbOpenHelper(context: Context)
         /**
          * Version of the database.
          */
-        const val DATABASE_VERSION = 2
+        const val DATABASE_VERSION = 3
     }
 
     override fun onCreate(db: SQLiteDatabase) = with(db) {
@@ -26,11 +26,13 @@ class DbOpenHelper(context: Context)
         execSQL(MangaSyncTable.createTableQuery)
         execSQL(CategoryTable.createTableQuery)
         execSQL(MangaCategoryTable.createTableQuery)
+        execSQL(HistoryTable.createTableQuery)
 
         // DB indexes
         execSQL(MangaTable.createUrlIndexQuery)
         execSQL(MangaTable.createFavoriteIndexQuery)
         execSQL(ChapterTable.createMangaIdIndexQuery)
+        execSQL(HistoryTable.createChapterIdIndexQuery)
     }
 
     override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
@@ -41,10 +43,15 @@ class DbOpenHelper(context: Context)
             db.execSQL("""UPDATE mangas SET thumbnail_url =
                     REPLACE(thumbnail_url, '93.174.95.110', 'kissmanga.com') WHERE source = 4""")
         }
+        if (oldVersion < 3) {
+            // Initialize history tables
+            db.execSQL(HistoryTable.createTableQuery)
+            db.execSQL(HistoryTable.createChapterIdIndexQuery)
+        }
     }
 
     override fun onConfigure(db: SQLiteDatabase) {
         db.setForeignKeyConstraintsEnabled(true)
     }
 
-}
+}

+ 58 - 0
app/src/main/java/eu/kanade/tachiyomi/data/database/models/History.java

@@ -0,0 +1,58 @@
+package eu.kanade.tachiyomi.data.database.models;
+
+import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteColumn;
+import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteType;
+
+import java.io.Serializable;
+
+import eu.kanade.tachiyomi.data.database.tables.HistoryTable;
+
+/**
+ * Object containing the history statistics of a chapter
+ */
+@StorIOSQLiteType(table = HistoryTable.TABLE)
+public class History implements Serializable {
+
+    /**
+     * Id of history object.
+     */
+    @StorIOSQLiteColumn(name = HistoryTable.COL_ID, key = true)
+    public Long id;
+
+    /**
+     * Chapter id of history object.
+     */
+    @StorIOSQLiteColumn(name = HistoryTable.COL_CHAPTER_ID)
+    public long chapter_id;
+
+    /**
+     * Last time chapter was read in time long format
+     */
+    @StorIOSQLiteColumn(name = HistoryTable.COL_LAST_READ)
+    public long last_read;
+
+    /**
+     * Total time chapter was read - todo not yet implemented
+     */
+    @StorIOSQLiteColumn(name = HistoryTable.COL_TIME_READ)
+    public long time_read;
+
+    /**
+     * Empty history constructor
+     */
+    public History() {
+    }
+
+    /**
+     * History constructor
+     *
+     * @param chapter chapter object
+     * @return history object
+     */
+    public static History create(Chapter chapter) {
+        History history = new History();
+        history.chapter_id = chapter.id;
+        return history;
+    }
+}
+

+ 27 - 0
app/src/main/java/eu/kanade/tachiyomi/data/database/models/MangaChapterHistory.java

@@ -0,0 +1,27 @@
+package eu.kanade.tachiyomi.data.database.models;
+
+/**
+ * Object containing manga, chapter and history
+ */
+public class MangaChapterHistory {
+    /**
+     * Object containing manga and chapter
+     */
+    public MangaChapter mangaChapter;
+
+    /**
+     * Object containing history
+     */
+    public History history;
+
+    /**
+     * MangaChapterHistory constructor
+     *
+     * @param mangaChapter object containing manga and chapter
+     * @param history      object containing history
+     */
+    public MangaChapterHistory(MangaChapter mangaChapter, History history) {
+        this.mangaChapter = mangaChapter;
+        this.history = history;
+    }
+}

+ 53 - 0
app/src/main/java/eu/kanade/tachiyomi/data/database/queries/HistoryQueries.kt

@@ -0,0 +1,53 @@
+package eu.kanade.tachiyomi.data.database.queries
+
+import com.pushtorefresh.storio.sqlite.queries.RawQuery
+import eu.kanade.tachiyomi.data.database.DbProvider
+import eu.kanade.tachiyomi.data.database.models.History
+import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
+import eu.kanade.tachiyomi.data.database.resolvers.HistoryLastReadPutResolver
+import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterHistoryGetResolver
+import eu.kanade.tachiyomi.data.database.tables.HistoryTable
+import java.util.*
+
+interface HistoryQueries : DbProvider {
+
+    /**
+     * Insert history into database
+     * @param history object containing history information
+     */
+    fun insertHistory(history: History) = db.put().`object`(history).prepare()
+
+    /**
+     * Returns history of recent manga containing last read chapter
+     * @param date recent date range
+     */
+    fun getRecentManga(date: Date) = db.get()
+            .listOfObjects(MangaChapterHistory::class.java)
+            .withQuery(RawQuery.builder()
+                    .query(getRecentMangasQuery())
+                    .args(date.time)
+                    .observesTables(HistoryTable.TABLE)
+                    .build())
+            .withGetResolver(MangaChapterHistoryGetResolver.INSTANCE)
+            .prepare()
+
+    fun getHistoryByMangaId(mangaId: Long) = db.get()
+            .listOfObjects(History::class.java)
+            .withQuery(RawQuery.builder()
+                    .query(getHistoryByMangaId())
+                    .args(mangaId)
+                    .observesTables(HistoryTable.TABLE)
+                    .build())
+            .prepare()
+
+
+    /**
+     * Updates the history last read.
+     * Inserts history object if not yet in database
+     * @param history history object
+     */
+    fun updateHistoryLastRead(history: History) = db.put()
+            .`object`(history)
+            .withPutResolver(HistoryLastReadPutResolver())
+            .prepare()
+}

+ 33 - 0
app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt

@@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.data.database.queries
 
 import eu.kanade.tachiyomi.data.database.tables.CategoryTable as Category
 import eu.kanade.tachiyomi.data.database.tables.ChapterTable as Chapter
+import eu.kanade.tachiyomi.data.database.tables.HistoryTable as History
 import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable as MangaCategory
 import eu.kanade.tachiyomi.data.database.tables.MangaTable as Manga
 
@@ -39,6 +40,38 @@ fun getRecentsQuery() = """
     ORDER BY ${Chapter.COL_DATE_UPLOAD} DESC
 """
 
+/**
+ * Query to get the recently read chapters of manga from the library up to a date.
+ * The max_last_read table contains the most recent chapters grouped by manga
+ * The select statement returns all information of chapters that have the same id as the chapter in max_last_read
+ * and are read after the given time period
+ * @return return limit is 25
+ */
+fun getRecentMangasQuery() = """
+    SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, ${Manga.TABLE}.*, ${Chapter.TABLE}.*, ${History.TABLE}.*
+    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}
+    JOIN (
+    SELECT ${Chapter.TABLE}.${Chapter.COL_MANGA_ID},${Chapter.TABLE}.${Chapter.COL_ID} as ${History.COL_CHAPTER_ID}, MAX(${History.TABLE}.${History.COL_LAST_READ}) as ${History.COL_LAST_READ}
+    FROM ${Chapter.TABLE} JOIN ${History.TABLE}
+    ON ${Chapter.TABLE}.${Chapter.COL_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}
+    GROUP BY ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}) AS max_last_read
+    ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = max_last_read.${Chapter.COL_MANGA_ID}
+    WHERE ${History.TABLE}.${History.COL_LAST_READ} > ? AND max_last_read.${History.COL_CHAPTER_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}
+    ORDER BY max_last_read.${History.COL_LAST_READ} DESC
+    LIMIT 25
+"""
+
+fun getHistoryByMangaId() = """
+    SELECT ${History.TABLE}.*
+    FROM ${History.TABLE}
+    JOIN ${Chapter.TABLE}
+    ON ${History.TABLE}.${History.COL_CHAPTER_ID} = ${Chapter.TABLE}.${Chapter.COL_ID}
+    WHERE ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = ? AND ${History.TABLE}.${History.COL_CHAPTER_ID} = ${Chapter.TABLE}.${Chapter.COL_ID}
+"""
 
 /**
  * Query to get the categories for a manga.

+ 52 - 0
app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/HistoryLastReadPutResolver.kt

@@ -0,0 +1,52 @@
+package eu.kanade.tachiyomi.data.database.resolvers
+
+import android.content.ContentValues
+import android.support.annotation.NonNull
+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.History
+import eu.kanade.tachiyomi.data.database.tables.HistoryTable
+
+class HistoryLastReadPutResolver : PutResolver<History>() {
+
+    /**
+     * Updates last_read time of chapter
+     */
+    override fun performPut(@NonNull db: StorIOSQLite, @NonNull history: History): PutResult = db.inTransactionReturn {
+        // Create put query
+        val updateQuery = mapToUpdateQuery(history)
+        val contentValues = mapToContentValues(history)
+
+        // Execute query
+        val numberOfRowsUpdated = db.internal().update(updateQuery, contentValues)
+
+        // If chapter not found in history insert into database
+        if (numberOfRowsUpdated == 0) {
+            db.put().`object`(history).prepare().asRxObservable().subscribe()
+        }
+        // Update result
+        PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
+    }
+
+    /**
+     * Creates update query
+     * @param history object
+     */
+    fun mapToUpdateQuery(history: History) = UpdateQuery.builder()
+            .table(HistoryTable.TABLE)
+            .where("${HistoryTable.COL_CHAPTER_ID} = ?")
+            .whereArgs(history.chapter_id)
+            .build()
+
+    /**
+     * Create content query
+     * @param history object
+     */
+    fun mapToContentValues(history: History) = ContentValues(1).apply {
+        put(HistoryTable.COL_LAST_READ, history.last_read)
+    }
+
+}

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaChapterGetResolver.kt

@@ -20,7 +20,7 @@ class MangaChapterGetResolver : DefaultGetResolver<MangaChapter>() {
         val manga = mangaGetResolver.mapFromCursor(cursor)
         val chapter = chapterGetResolver.mapFromCursor(cursor)
         manga.id = chapter.manga_id
-        manga.url = cursor.getString(cursor.getColumnIndex("mangaUrl"));
+        manga.url = cursor.getString(cursor.getColumnIndex("mangaUrl"))
 
         return MangaChapter(manga, chapter)
     }

+ 51 - 0
app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/MangaChapterHistoryGetResolver.kt

@@ -0,0 +1,51 @@
+package eu.kanade.tachiyomi.data.database.resolvers
+
+import android.database.Cursor
+import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
+import eu.kanade.tachiyomi.data.database.models.*
+
+class MangaChapterHistoryGetResolver : DefaultGetResolver<MangaChapterHistory>() {
+    companion object {
+        val INSTANCE = MangaChapterHistoryGetResolver()
+    }
+
+    /**
+     * Manga get resolver
+     */
+    private val mangaGetResolver = MangaStorIOSQLiteGetResolver()
+
+    /**
+     * Chapter get resolver
+     */
+    private val chapterResolver = ChapterStorIOSQLiteGetResolver()
+
+    /**
+     * History get resolver
+     */
+    private val historyGetResolver = HistoryStorIOSQLiteGetResolver()
+
+    /**
+     * Map correct objects from cursor result
+     */
+    override fun mapFromCursor(cursor: Cursor): MangaChapterHistory {
+        // Get manga object
+        val manga = mangaGetResolver.mapFromCursor(cursor)
+
+        // Get chapter object
+        val chapter = chapterResolver.mapFromCursor(cursor)
+
+        // Get history object
+        val history = historyGetResolver.mapFromCursor(cursor)
+
+        // Make certain column conflicts are dealt with
+        manga.id = chapter.manga_id
+        manga.url = cursor.getString(cursor.getColumnIndex("mangaUrl"))
+        chapter.id = history.chapter_id
+
+        // Create mangaChapter object
+        val mangaChapter = MangaChapter(manga, chapter)
+
+        // Return result
+        return MangaChapterHistory(mangaChapter, history)
+    }
+}

+ 48 - 0
app/src/main/java/eu/kanade/tachiyomi/data/database/tables/HistoryTable.kt

@@ -0,0 +1,48 @@
+package eu.kanade.tachiyomi.data.database.tables
+
+object HistoryTable {
+
+    /**
+     * Table name
+     */
+    const val TABLE = "history"
+
+    /**
+     * Id column name
+     */
+    const val COL_ID = "${TABLE}_id"
+
+    /**
+     * Chapter id column name
+     */
+    const val COL_CHAPTER_ID = "${TABLE}_chapter_id"
+
+    /**
+     * Last read column name
+     */
+    const val COL_LAST_READ = "${TABLE}_last_read"
+
+    /**
+     * Time read column name
+     */
+    const val COL_TIME_READ = "${TABLE}_time_read"
+
+    /**
+     * query to create history table
+     */
+    val createTableQuery: String
+        get() = """CREATE TABLE $TABLE(
+            $COL_ID INTEGER NOT NULL PRIMARY KEY,
+            $COL_CHAPTER_ID INTEGER NOT NULL UNIQUE,
+            $COL_LAST_READ LONG,
+            $COL_TIME_READ LONG,
+            FOREIGN KEY($COL_CHAPTER_ID) REFERENCES ${ChapterTable.TABLE} (${ChapterTable.COL_ID})
+            ON DELETE CASCADE
+            )"""
+
+    /**
+     * query to index history chapter id
+     */
+    val createChapterIdIndexQuery: String
+        get() = "CREATE INDEX ${TABLE}_${COL_CHAPTER_ID}_index ON $TABLE($COL_CHAPTER_ID)"
+}

+ 3 - 1
app/src/main/java/eu/kanade/tachiyomi/injection/component/AppComponent.kt

@@ -24,7 +24,8 @@ import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersPresenter
 import eu.kanade.tachiyomi.ui.manga.info.MangaInfoPresenter
 import eu.kanade.tachiyomi.ui.manga.myanimelist.MyAnimeListPresenter
 import eu.kanade.tachiyomi.ui.reader.ReaderPresenter
-import eu.kanade.tachiyomi.ui.recent.RecentChaptersPresenter
+import eu.kanade.tachiyomi.ui.recent_updates.RecentChaptersPresenter
+import eu.kanade.tachiyomi.ui.recently_read.RecentlyReadPresenter
 import eu.kanade.tachiyomi.ui.setting.SettingsActivity
 import javax.inject.Singleton
 
@@ -42,6 +43,7 @@ interface AppComponent {
     fun inject(myAnimeListPresenter: MyAnimeListPresenter)
     fun inject(categoryPresenter: CategoryPresenter)
     fun inject(recentChaptersPresenter: RecentChaptersPresenter)
+    fun inject(recentlyReadPresenter: RecentlyReadPresenter)
     fun inject(backupPresenter: BackupPresenter)
 
     fun inject(mainActivity: MainActivity)

+ 3 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt

@@ -13,7 +13,8 @@ import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
 import eu.kanade.tachiyomi.ui.catalogue.CatalogueFragment
 import eu.kanade.tachiyomi.ui.download.DownloadFragment
 import eu.kanade.tachiyomi.ui.library.LibraryFragment
-import eu.kanade.tachiyomi.ui.recent.RecentChaptersFragment
+import eu.kanade.tachiyomi.ui.recent_updates.RecentChaptersFragment
+import eu.kanade.tachiyomi.ui.recently_read.RecentlyReadFragment
 import eu.kanade.tachiyomi.ui.setting.SettingsActivity
 import kotlinx.android.synthetic.main.activity_main.*
 import kotlinx.android.synthetic.main.toolbar.*
@@ -50,6 +51,7 @@ class MainActivity : BaseActivity() {
             when (item.itemId) {
                 R.id.nav_drawer_library -> setFragment(LibraryFragment.newInstance())
                 R.id.nav_drawer_recent_updates -> setFragment(RecentChaptersFragment.newInstance())
+                R.id.nav_drawer_recent_manga -> setFragment(RecentlyReadFragment.newInstance())
                 R.id.nav_drawer_catalogues -> setFragment(CatalogueFragment.newInstance())
                 R.id.nav_drawer_downloads -> setFragment(DownloadFragment.newInstance())
                 R.id.nav_drawer_settings -> startActivity(Intent(this, SettingsActivity::class.java))

+ 9 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt

@@ -4,6 +4,7 @@ import android.os.Bundle
 import eu.kanade.tachiyomi.data.cache.ChapterCache
 import eu.kanade.tachiyomi.data.database.DatabaseHelper
 import eu.kanade.tachiyomi.data.database.models.Chapter
+import eu.kanade.tachiyomi.data.database.models.History
 import eu.kanade.tachiyomi.data.database.models.Manga
 import eu.kanade.tachiyomi.data.database.models.MangaSync
 import eu.kanade.tachiyomi.data.download.DownloadManager
@@ -24,6 +25,7 @@ import rx.schedulers.Schedulers
 import rx.subjects.PublishSubject
 import timber.log.Timber
 import java.io.File
+import java.util.*
 import javax.inject.Inject
 
 class ReaderPresenter : BasePresenter<ReaderActivity>() {
@@ -289,6 +291,7 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
         }
     }
 
+
     // Check whether the given chapter is downloaded
     fun isChapterDownloaded(chapter: Chapter): Boolean {
         return downloadManager.isChapterDownloaded(source, manga, chapter)
@@ -340,6 +343,12 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
             }
         }
         db.updateChapterProgress(chapter).asRxObservable().subscribe()
+        // Update last read data
+        db.updateHistoryLastRead(History.create(chapter)
+                .apply { last_read = Date().time })
+                .asRxObservable()
+                .doOnError { Timber.e(it.message) }
+                .subscribe()
     }
 
     /**

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/recent/RecentChaptersAdapter.kt → app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersAdapter.kt

@@ -1,4 +1,4 @@
-package eu.kanade.tachiyomi.ui.recent
+package eu.kanade.tachiyomi.ui.recent_updates
 
 import android.support.v7.widget.RecyclerView
 import android.view.View
@@ -72,7 +72,7 @@ class RecentChaptersAdapter(val fragment: RecentChaptersFragment) : FlexibleAdap
         // Check which view type and set correct values.
         when (viewType) {
             VIEW_TYPE_CHAPTER -> {
-                view = parent.inflate(R.layout.item_recent_chapter)
+                view = parent.inflate(R.layout.item_recent_chapters)
                 return RecentChaptersHolder(view, this, fragment)
             }
             VIEW_TYPE_SECTION -> {

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/recent/RecentChaptersFragment.kt → app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersFragment.kt

@@ -1,4 +1,4 @@
-package eu.kanade.tachiyomi.ui.recent
+package eu.kanade.tachiyomi.ui.recent_updates
 
 import android.os.Bundle
 import android.support.v4.app.DialogFragment
@@ -213,7 +213,7 @@ class RecentChaptersFragment : BaseRxFragment<RecentChaptersPresenter>(), Action
      */
     fun onNextMangaChapters(chapters: List<Any>) {
         (activity as MainActivity).updateEmptyView(chapters.isEmpty(),
-                R.string.information_no_recent, R.drawable.ic_history_black_128dp)
+                R.string.information_no_recent, R.drawable.ic_update_black_128dp)
 
         destroyActionModeIfNeeded()
         adapter.setItems(chapters)

+ 4 - 4
app/src/main/java/eu/kanade/tachiyomi/ui/recent/RecentChaptersHolder.kt → app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersHolder.kt

@@ -1,4 +1,4 @@
-package eu.kanade.tachiyomi.ui.recent
+package eu.kanade.tachiyomi.ui.recent_updates
 
 import android.view.View
 import android.widget.PopupMenu
@@ -7,11 +7,11 @@ import eu.kanade.tachiyomi.data.database.models.MangaChapter
 import eu.kanade.tachiyomi.data.download.model.Download
 import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
 import eu.kanade.tachiyomi.util.getResourceColor
-import kotlinx.android.synthetic.main.item_recent_chapter.view.*
+import kotlinx.android.synthetic.main.item_recent_chapters.view.*
 
 /**
  * Holder that contains chapter item
- * Uses R.layout.item_recent_chapter.
+ * Uses R.layout.item_recent_chapters.
  * UI related actions should be called from here.
  *
  * @param view the inflated view for this holder.
@@ -19,7 +19,7 @@ import kotlinx.android.synthetic.main.item_recent_chapter.view.*
  * @param listener a listener to react to single tap and long tap events.
  * @constructor creates a new recent chapter holder.
  */
-class RecentChaptersHolder(view: View, private val adapter: RecentChaptersAdapter, listener: FlexibleViewHolder.OnListItemClickListener) :
+class RecentChaptersHolder(view: View, private val adapter: RecentChaptersAdapter, listener: OnListItemClickListener) :
         FlexibleViewHolder(view, adapter, listener) {
     /**
      * Color of read chapter

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/recent/RecentChaptersPresenter.kt → app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/RecentChaptersPresenter.kt

@@ -1,4 +1,4 @@
-package eu.kanade.tachiyomi.ui.recent
+package eu.kanade.tachiyomi.ui.recent_updates
 
 import android.os.Bundle
 import eu.kanade.tachiyomi.data.database.DatabaseHelper

+ 1 - 1
app/src/main/java/eu/kanade/tachiyomi/ui/recent/SectionViewHolder.kt → app/src/main/java/eu/kanade/tachiyomi/ui/recent_updates/SectionViewHolder.kt

@@ -1,4 +1,4 @@
-package eu.kanade.tachiyomi.ui.recent
+package eu.kanade.tachiyomi.ui.recent_updates
 
 import android.support.v7.widget.RecyclerView
 import android.text.format.DateUtils

+ 52 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadAdapter.kt

@@ -0,0 +1,52 @@
+package eu.kanade.tachiyomi.ui.recently_read
+
+import android.support.v7.widget.RecyclerView
+import android.view.ViewGroup
+import eu.davidea.flexibleadapter.FlexibleAdapter
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
+import eu.kanade.tachiyomi.util.inflate
+
+/**
+ * Adapter of RecentlyReadHolder.
+ * Connection between Fragment and Holder
+ * Holder updates should be called from here.
+ *
+ * @param fragment a RecentlyReadFragment object
+ * @constructor creates an instance of the adapter.
+ */
+class RecentlyReadAdapter(val fragment: RecentlyReadFragment) : FlexibleAdapter<RecyclerView.ViewHolder, Any>() {
+    /**
+     * Called when ViewHolder is created
+     * @param parent parent View
+     * @param viewType int containing viewType
+     */
+    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder? {
+        val view = parent.inflate(R.layout.item_recent_manga)
+        return RecentlyReadHolder(view, this)
+    }
+
+    /**
+     * Called when ViewHolder is bind
+     * @param holder bind holder
+     * @param position position of holder
+     */
+    override fun onBindViewHolder(holder: RecyclerView.ViewHolder?, position: Int) {
+        val item = getItem(position) as MangaChapterHistory
+        (holder as RecentlyReadHolder).onSetValues(item)
+    }
+
+    /**
+     * Update items
+     * @param items items
+     */
+    fun setItems(items: List<MangaChapterHistory>) {
+        mItems = items
+        notifyDataSetChanged()
+    }
+
+    override fun updateDataSet(param: String?) {
+        // Empty function
+    }
+
+}

+ 129 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadFragment.kt

@@ -0,0 +1,129 @@
+package eu.kanade.tachiyomi.ui.recently_read
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.Chapter
+import eu.kanade.tachiyomi.data.database.models.History
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
+import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
+import eu.kanade.tachiyomi.ui.main.MainActivity
+import eu.kanade.tachiyomi.ui.manga.MangaActivity
+import eu.kanade.tachiyomi.ui.reader.ReaderActivity
+import eu.kanade.tachiyomi.widget.NpaLinearLayoutManager
+import kotlinx.android.synthetic.main.fragment_recent_manga.*
+import nucleus.factory.RequiresPresenter
+
+/**
+ * Fragment that shows recently read manga.
+ * Uses R.layout.fragment_recent_manga.
+ * UI related actions should be called from here.
+ */
+@RequiresPresenter(RecentlyReadPresenter::class)
+class RecentlyReadFragment : BaseRxFragment<RecentlyReadPresenter>() {
+    companion object {
+        /**
+         * Create new RecentChaptersFragment.
+         *
+         */
+        @JvmStatic
+        fun newInstance(): RecentlyReadFragment {
+            return RecentlyReadFragment()
+        }
+    }
+
+    /**
+     * Adapter containing the recent manga.
+     */
+    lateinit var adapter: RecentlyReadAdapter
+        private set
+
+    /**
+     * Called when view gets created
+     *
+     * @param inflater layout inflater
+     * @param container view group
+     * @param savedState status of saved state
+     */
+    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View? {
+        return inflater.inflate(R.layout.fragment_recent_manga, container, false)
+    }
+
+    /**
+     * Called when view is created
+     *
+     * @param view created view
+     * @param savedInstanceState status of saved sate
+     */
+    override fun onViewCreated(view: View?, savedInstanceState: Bundle?) {
+        // Initialize adapter
+        recycler.layoutManager = NpaLinearLayoutManager(activity)
+        adapter = RecentlyReadAdapter(this)
+        recycler.setHasFixedSize(true)
+        recycler.adapter = adapter
+
+        // Update toolbar text
+        setToolbarTitle(R.string.label_recent_manga)
+    }
+
+    /**
+     * Populate adapter with chapters
+     *
+     * @param mangaHistory list of manga history
+     */
+    fun onNextManga(mangaHistory: List<MangaChapterHistory>) {
+        (activity as MainActivity).updateEmptyView(mangaHistory.isEmpty(),
+                R.string.information_no_recent_manga, R.drawable.ic_glasses_black_128dp)
+
+        adapter.setItems(mangaHistory)
+    }
+
+    /**
+     * Reset last read of chapter to 0L
+     * @param history history belonging to chapter
+     */
+    fun removeFromHistory(history: History) {
+        presenter.removeFromHistory(history)
+        adapter.notifyDataSetChanged()
+    }
+
+    /**
+     * Removes all chapters belonging to manga from library
+     * @param mangaId id of manga
+     */
+    fun removeAllFromHistory(mangaId: Long) {
+        presenter.removeAllFromHistory(mangaId)
+        adapter.notifyDataSetChanged()
+    }
+
+    /**
+     * Open chapter to continue reading
+     * @param chapter chapter that is opened
+     * @param manga manga belonging to chapter
+     */
+    fun openChapter(chapter: Chapter, manga: Manga) {
+        val intent = ReaderActivity.newIntent(activity, manga, chapter)
+        startActivity(intent)
+    }
+
+    /**
+     * Open manga info page
+     * @param manga manga belonging to info page
+     */
+    fun openMangaInfo(manga: Manga) {
+        val intent = MangaActivity.newIntent(activity, manga, true)
+        startActivity(intent)
+    }
+
+    /**
+     * Returns the timestamp of last read
+     * @param history history containing time of last read
+     */
+    fun getLastRead(history: History): String? {
+        return presenter.getLastRead(history)
+    }
+
+}

+ 96 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadHolder.kt

@@ -0,0 +1,96 @@
+package eu.kanade.tachiyomi.ui.recently_read
+
+import android.support.v7.widget.RecyclerView
+import android.view.View
+import com.afollestad.materialdialogs.MaterialDialog
+import com.bumptech.glide.Glide
+import com.bumptech.glide.load.engine.DiskCacheStrategy
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
+import eu.kanade.tachiyomi.data.source.SourceManager
+import kotlinx.android.synthetic.main.dialog_remove_recently.view.*
+import kotlinx.android.synthetic.main.item_recent_manga.view.*
+import java.text.DecimalFormat
+import java.text.DecimalFormatSymbols
+
+/**
+ * Holder that contains recent manga item
+ * Uses R.layout.item_recent_manga.
+ * UI related actions should be called from here.
+ *
+ * @param view the inflated view for this holder.
+ * @param adapter the adapter handling this holder.
+ * @constructor creates a new recent chapter holder.
+ */
+class RecentlyReadHolder(view: View, private val adapter: RecentlyReadAdapter) :
+        RecyclerView.ViewHolder(view) {
+
+    /**
+     * DecimalFormat used to display correct chapter number
+     */
+    private val decimalFormat = DecimalFormat("#.###", DecimalFormatSymbols().apply { decimalSeparator = '.' })
+
+    /**
+     * Set values of view
+     *
+     * @param item item containing history information
+     */
+    fun onSetValues(item: MangaChapterHistory) {
+        // Retrieve objects
+        val manga = item.mangaChapter.manga
+        val chapter = item.mangaChapter.chapter
+        val history = item.history
+
+        // Set manga title
+        itemView.manga_title.text = manga.title
+
+        // Set source + chapter title
+        val formattedNumber = decimalFormat.format(chapter.chapter_number.toDouble())
+        itemView.manga_source.text = itemView.context.getString(R.string.recent_manga_source)
+                .format(SourceManager(adapter.fragment.context).get(manga.source)?.name, formattedNumber)
+
+        // Set last read timestamp title
+        itemView.last_read.text = adapter.fragment.getLastRead(history)
+
+        // Set cover
+        if (!manga.thumbnail_url.isNullOrEmpty()) {
+            Glide.with(itemView.context)
+                    .load(manga)
+                    .diskCacheStrategy(DiskCacheStrategy.RESULT)
+                    .centerCrop()
+                    .into(itemView.cover)
+        }
+
+        // Set remove clickListener
+        itemView.remove.setOnClickListener {
+            MaterialDialog.Builder(itemView.context)
+                    .title(R.string.action_remove)
+                    .customView(R.layout.dialog_remove_recently, true)
+                    .positiveText(R.string.action_remove)
+                    .negativeText(android.R.string.cancel)
+                    .onPositive { materialDialog, dialogAction ->
+                        // Check if user wants all chapters reset
+                        if (materialDialog.customView?.removeAll?.isChecked as Boolean) {
+                            adapter.fragment.removeAllFromHistory(manga.id)
+                        } else {
+                            adapter.fragment.removeFromHistory(history)
+                        }
+                    }
+                    .onNegative { materialDialog, dialogAction ->
+                        materialDialog.dismiss()
+                    }
+                    .show();
+        }
+
+        // Set continue reading clickListener
+        itemView.resume.setOnClickListener {
+            adapter.fragment.openChapter(chapter, manga)
+        }
+
+        // Set open manga info clickListener
+        itemView.cover.setOnClickListener {
+            adapter.fragment.openMangaInfo(manga)
+        }
+    }
+
+}

+ 97 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/recently_read/RecentlyReadPresenter.kt

@@ -0,0 +1,97 @@
+package eu.kanade.tachiyomi.ui.recently_read
+
+import android.os.Bundle
+import eu.kanade.tachiyomi.data.database.DatabaseHelper
+import eu.kanade.tachiyomi.data.database.models.History
+import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
+import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
+import rx.Observable
+import rx.android.schedulers.AndroidSchedulers
+import rx.schedulers.Schedulers
+import timber.log.Timber
+import java.text.SimpleDateFormat
+import java.util.*
+import javax.inject.Inject
+
+/**
+ * The id of the restartable.
+ */
+const private val GET_RECENT_MANGA = 1
+
+/**
+ * Presenter of RecentlyReadFragment.
+ * Contains information and data for fragment.
+ * Observable updates should be called from here.
+ */
+class RecentlyReadPresenter : BasePresenter<RecentlyReadFragment>() {
+    /**
+     * Used to connect to database
+     */
+    @Inject lateinit var db: DatabaseHelper
+
+    override fun onCreate(savedState: Bundle?) {
+        super.onCreate(savedState)
+
+        // Used to get recent manga
+        restartableLatestCache(GET_RECENT_MANGA,
+                { getRecentMangaObservable() },
+                { recentMangaFragment, manga ->
+                    // Update adapter to show recent manga's
+                    recentMangaFragment.onNextManga(manga)
+                }
+        )
+
+        if (savedState == null) {
+            // Start fetching recent manga
+            start(GET_RECENT_MANGA)
+        }
+    }
+
+    /**
+     * Get recent manga observable
+     * @return list of history
+     */
+    fun getRecentMangaObservable(): Observable<MutableList<MangaChapterHistory>> {
+        // Set date for recent manga
+        val cal = Calendar.getInstance()
+        cal.time = Date()
+        cal.add(Calendar.MONTH, -1)
+
+        return db.getRecentManga(cal.time).asRxObservable()
+                .observeOn(AndroidSchedulers.mainThread())
+    }
+
+    /**
+     * Reset last read of chapter to 0L
+     * @param history history belonging to chapter
+     */
+    fun removeFromHistory(history: History) {
+        history.last_read = 0L
+        db.updateHistoryLastRead(history).asRxObservable()
+                .doOnError { Timber.e(it.message) }.subscribe()
+    }
+
+    /**
+     * Removes all chapters belonging to manga from library
+     * @param mangaId id of manga
+     */
+    fun removeAllFromHistory(mangaId: Long) {
+        db.getHistoryByMangaId(mangaId).asRxObservable()
+                .take(1)
+                .flatMapIterable { it }
+                .doOnError { Timber.e(it.message) }
+                .subscribeOn(Schedulers.io())
+                .observeOn(AndroidSchedulers.mainThread())
+                .subscribe({ result -> removeFromHistory(result) })
+    }
+
+    /**
+     * Returns the timestamp of last read
+     * @param history history containing time of last read
+     */
+    fun getLastRead(history: History): String? {
+        return SimpleDateFormat("dd-MM-yyyy HH:mm",
+                Locale.getDefault()).format(Date(history.last_read))
+    }
+
+}

+ 9 - 0
app/src/main/res/drawable/ic_glasses_black_128dp.xml

@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="128dp"
+        android:height="128dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M3,10C2.76,10 2.55,10.09 2.41,10.25C2.27,10.4 2.21,10.62 2.24,10.86L2.74,13.85C2.82,14.5 3.4,15 4,15H7C7.64,15 8.36,14.44 8.5,13.82L9.56,10.63C9.6,10.5 9.57,10.31 9.5,10.19C9.39,10.07 9.22,10 9,10H3M7,17H4C2.38,17 0.96,15.74 0.76,14.14L0.26,11.15C0.15,10.3 0.39,9.5 0.91,8.92C1.43,8.34 2.19,8 3,8H9C9.83,8 10.58,8.35 11.06,8.96C11.17,9.11 11.27,9.27 11.35,9.45C11.78,9.36 12.22,9.36 12.64,9.45C12.72,9.27 12.82,9.11 12.94,8.96C13.41,8.35 14.16,8 15,8H21C21.81,8 22.57,8.34 23.09,8.92C23.6,9.5 23.84,10.3 23.74,11.11L23.23,14.18C23.04,15.74 21.61,17 20,17H17C15.44,17 13.92,15.81 13.54,14.3L12.64,11.59C12.26,11.31 11.73,11.31 11.35,11.59L10.43,14.37C10.07,15.82 8.56,17 7,17M15,10C14.78,10 14.61,10.07 14.5,10.19C14.42,10.31 14.4,10.5 14.45,10.7L15.46,13.75C15.64,14.44 16.36,15 17,15H20C20.59,15 21.18,14.5 21.25,13.89L21.76,10.82C21.79,10.62 21.73,10.4 21.59,10.25C21.45,10.09 21.24,10 21,10H15Z"/>
+</vector>

+ 9 - 0
app/src/main/res/drawable/ic_glasses_black_24dp.xml

@@ -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="#FF000000"
+        android:pathData="M3,10C2.76,10 2.55,10.09 2.41,10.25C2.27,10.4 2.21,10.62 2.24,10.86L2.74,13.85C2.82,14.5 3.4,15 4,15H7C7.64,15 8.36,14.44 8.5,13.82L9.56,10.63C9.6,10.5 9.57,10.31 9.5,10.19C9.39,10.07 9.22,10 9,10H3M7,17H4C2.38,17 0.96,15.74 0.76,14.14L0.26,11.15C0.15,10.3 0.39,9.5 0.91,8.92C1.43,8.34 2.19,8 3,8H9C9.83,8 10.58,8.35 11.06,8.96C11.17,9.11 11.27,9.27 11.35,9.45C11.78,9.36 12.22,9.36 12.64,9.45C12.72,9.27 12.82,9.11 12.94,8.96C13.41,8.35 14.16,8 15,8H21C21.81,8 22.57,8.34 23.09,8.92C23.6,9.5 23.84,10.3 23.74,11.11L23.23,14.18C23.04,15.74 21.61,17 20,17H17C15.44,17 13.92,15.81 13.54,14.3L12.64,11.59C12.26,11.31 11.73,11.31 11.35,11.59L10.43,14.37C10.07,15.82 8.56,17 7,17M15,10C14.78,10 14.61,10.07 14.5,10.19C14.42,10.31 14.4,10.5 14.45,10.7L15.46,13.75C15.64,14.44 16.36,15 17,15H20C20.59,15 21.18,14.5 21.25,13.89L21.76,10.82C21.79,10.62 21.73,10.4 21.59,10.25C21.45,10.09 21.24,10 21,10H15Z"/>
+</vector>

+ 0 - 9
app/src/main/res/drawable/ic_history_black_128dp.xml

@@ -1,9 +0,0 @@
-<vector xmlns:android="http://schemas.android.com/apk/res/android"
-        android:width="128dp"
-        android:height="128dp"
-        android:viewportWidth="24.0"
-        android:viewportHeight="24.0">
-    <path
-        android:fillColor="#FF000000"
-        android:pathData="M13,3c-4.97,0 -9,4.03 -9,9L1,12l3.89,3.89 0.07,0.14L9,12L6,12c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.93,0 -3.68,-0.79 -4.94,-2.06l-1.42,1.42C8.27,19.99 10.51,21 13,21c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,8v5l4.28,2.54 0.72,-1.21 -3.5,-2.08L13.5,8L12,8z"/>
-</vector>

+ 0 - 9
app/src/main/res/drawable/ic_history_black_24dp.xml

@@ -1,9 +0,0 @@
-<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="#FF000000"
-        android:pathData="M13,3c-4.97,0 -9,4.03 -9,9L1,12l3.89,3.89 0.07,0.14L9,12L6,12c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.93,0 -3.68,-0.79 -4.94,-2.06l-1.42,1.42C8.27,19.99 10.51,21 13,21c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,8v5l4.28,2.54 0.72,-1.21 -3.5,-2.08L13.5,8L12,8z"/>
-</vector>

+ 9 - 0
app/src/main/res/drawable/ic_update_black_128dp.xml

@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="128dp"
+        android:height="128dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M21,10.12h-6.78l2.74,-2.82c-2.73,-2.7 -7.15,-2.8 -9.88,-0.1 -2.73,2.71 -2.73,7.08 0,9.79 2.73,2.71 7.15,2.71 9.88,0C18.32,15.65 19,14.08 19,12.1h2c0,1.98 -0.88,4.55 -2.64,6.29 -3.51,3.48 -9.21,3.48 -12.72,0 -3.5,-3.47 -3.53,-9.11 -0.02,-12.58 3.51,-3.47 9.14,-3.47 12.65,0L21,3v7.12zM12.5,8v4.25l3.5,2.08 -0.72,1.21L11,13V8h1.5z"/>
+</vector>

+ 9 - 0
app/src/main/res/drawable/ic_update_black_24dp.xml

@@ -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="#FF000000"
+        android:pathData="M21,10.12h-6.78l2.74,-2.82c-2.73,-2.7 -7.15,-2.8 -9.88,-0.1 -2.73,2.71 -2.73,7.08 0,9.79 2.73,2.71 7.15,2.71 9.88,0C18.32,15.65 19,14.08 19,12.1h2c0,1.98 -0.88,4.55 -2.64,6.29 -3.51,3.48 -9.21,3.48 -12.72,0 -3.5,-3.47 -3.53,-9.11 -0.02,-12.58 3.51,-3.47 9.14,-3.47 12.65,0L21,3v7.12zM12.5,8v4.25l3.5,2.08 -0.72,1.21L11,13V8h1.5z"/>
+</vector>

+ 22 - 0
app/src/main/res/layout/dialog_remove_recently.xml

@@ -0,0 +1,22 @@
+<?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="wrap_content"
+              android:orientation="vertical"
+              android:padding="@dimen/activity_vertical_margin">
+
+    <TextView
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:text="@string/dialog_remove_recently_description"
+        android:textAppearance="@style/TextAppearance.Regular.Body1"/>
+
+
+    <CheckBox
+        android:id="@+id/removeAll"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="18dp"
+        android:text="@string/dialog_remove_recently_reset"/>
+
+</LinearLayout>

+ 3 - 1
app/src/main/res/layout/fragment_recent_chapters.xml

@@ -10,6 +10,8 @@
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:descendantFocusability="blocksDescendants"
-        tools:listitem="@layout/item_recent_chapter"/>
+        tools:listitem="@layout/item_recent_chapters">
+
+    </android.support.v7.widget.RecyclerView>
 
 </RelativeLayout>

+ 16 - 0
app/src/main/res/layout/fragment_recent_manga.xml

@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+                xmlns:tools="http://schemas.android.com/tools"
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:orientation="vertical">
+
+    <android.support.v7.widget.RecyclerView
+        android:id="@+id/recycler"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        tools:listitem="@layout/item_recent_manga">
+
+    </android.support.v7.widget.RecyclerView>
+
+</RelativeLayout>

+ 0 - 0
app/src/main/res/layout/item_recent_chapter.xml → app/src/main/res/layout/item_recent_chapters.xml


+ 82 - 0
app/src/main/res/layout/item_recent_manga.xml

@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.v7.widget.CardView
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/cv_manga"
+    style="@style/Theme.Widget.CardView"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    >
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="150dp"
+        android:orientation="horizontal">
+
+        <ImageView
+            android:id="@+id/cover"
+            android:layout_width="100dp"
+            android:layout_height="match_parent"
+            android:clickable="true"
+            android:contentDescription="@string/description_cover"
+            android:scaleType="centerCrop"/>
+
+        <RelativeLayout
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:orientation="vertical">
+
+            <RelativeLayout
+                android:layout_width="match_parent"
+                android:layout_height="match_parent"
+                android:orientation="vertical"
+                android:padding="@dimen/card_margin">
+
+                <TextView
+                    android:id="@+id/manga_title"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:maxLines="2"
+                    android:textAppearance="@style/TextAppearance.Medium.Title"/>
+
+                <TextView
+                    android:id="@+id/manga_source"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_below="@id/manga_title"
+                    android:textAppearance="@style/TextAppearance.Medium.Body2"/>
+
+                <TextView
+                    android:id="@+id/last_read"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:layout_below="@id/manga_source"
+                    android:textAppearance="@style/TextAppearance.Medium.Body2.Hint"/>
+            </RelativeLayout>
+
+            <TextView
+                android:id="@+id/remove"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_alignParentBottom="true"
+                android:layout_marginLeft="8dp"
+                android:layout_marginStart="8dp"
+                android:background="?attr/selectable_list_drawable"
+                android:clickable="true"
+                android:padding="8dp"
+                android:text="@string/action_remove"
+                android:textAppearance="@style/TextAppearance.Medium.Button.Negative"/>
+
+            <TextView
+                android:id="@+id/resume"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_alignParentBottom="true"
+                android:layout_toEndOf="@id/remove"
+                android:layout_toRightOf="@id/remove"
+                android:background="?attr/selectable_list_drawable"
+                android:padding="8dp"
+                android:text="@string/action_resume"
+                android:textAppearance="@style/TextAppearance.Medium.Button"/>
+        </RelativeLayout>
+    </LinearLayout>
+</android.support.v7.widget.CardView>

+ 6 - 1
app/src/main/res/menu/menu_navigation.xml

@@ -7,10 +7,15 @@
             android:id="@+id/nav_drawer_library"
             android:icon="@drawable/ic_book_black_24dp"
             android:title="@string/label_library" />
+        <item
+            android:id="@+id/nav_drawer_recent_manga"
+            android:icon="@drawable/ic_glasses_black_24dp"
+            android:title="@string/label_recent_manga"/>
         <item
             android:id="@+id/nav_drawer_recent_updates"
-            android:icon="@drawable/ic_history_black_24dp"
+            android:icon="@drawable/ic_update_black_24dp"
             android:title="@string/label_recent_updates" />
+
         <item
             android:id="@+id/nav_drawer_catalogues"
             android:icon="@drawable/ic_explore_black_24dp"

+ 8 - 0
app/src/main/res/values/arrays.xml

@@ -136,4 +136,12 @@
         <item>ac</item>
     </string-array>
 
+    <string-array name="remove_recently_read">
+        <item>@string/scale_type_fit_screen</item>
+    </string-array>
+
+    <string-array name="remove_recently_read_values">
+        <item>remove</item>
+    </string-array>
+
 </resources>

+ 2 - 0
app/src/main/res/values/colors.xml

@@ -70,4 +70,6 @@
 
     <color name="md_blue_grey_900">#263238</color>
 
+    <color name="md_red_500">#F44336</color>
+
 </resources>

+ 14 - 2
app/src/main/res/values/strings.xml

@@ -7,6 +7,7 @@
     <string name="label_settings">Settings</string>
     <string name="label_download_queue">Download queue</string>
     <string name="label_library">My library</string>
+    <string name="label_recent_manga">Recently read</string>
     <string name="label_recent_updates">Recent updates</string>
     <string name="label_catalogues">Catalogues</string>
     <string name="label_categories">Categories</string>
@@ -46,6 +47,8 @@
     <string name="action_previous_chapter">Previous chapter</string>
     <string name="action_next_chapter">Next chapter</string>
     <string name="action_retry">Retry</string>
+    <string name="action_remove">Remove</string>
+    <string name="action_resume">Resume</string>
     <string name="action_open_in_browser">Open in browser</string>
     <string name="action_display_mode">Change display mode</string>
     <string name="action_cancel">Cancel</string>
@@ -248,6 +251,10 @@
     <string name="status">Status</string>
     <string name="chapters">Chapters</string>
 
+    <!-- Dialog remove recently view -->
+    <string name="dialog_remove_recently_description">This will remove the read date of this chapter. Are you sure?</string>
+    <string name="dialog_remove_recently_reset">Reset all chapters for this manga</string>
+
 
     <!-- Reader activity -->
     <string name="downloading">Downloading…</string>
@@ -266,6 +273,9 @@
     <string name="backup_completed">Backup successfully restored</string>
     <string name="restore_please_wait">Restoring backup. Please wait…</string>
 
+    <!-- Recent manga fragment -->
+    <string name="recent_manga_source">%1$s - Ch.%2$s</string>
+
     <!-- Downloads activity and service -->
     <string name="download_queue_error">An error occurred while downloading chapters. You can try again in the downloads section</string>
 
@@ -302,12 +312,13 @@
     <string name="update_check_notification_update_available">Update available</string>
 
     <!--Content Description-->
-    <string name="description_backdrop">Backdrop image of selected manga</string>
-    <string name="description_cover">Cover of selected manga</string>
+    <string name="description_backdrop">Backdrop image of manga</string>
+    <string name="description_cover">Cover of manga</string>
 
     <!-- Information Text -->
     <string name="information_no_downloads">No downloads</string>
     <string name="information_no_recent">No recent chapters</string>
+    <string name="information_no_recent_manga">No recently read manga</string>
     <string name="information_empty_library">Empty library</string>
 
     <!-- Download Notification -->
@@ -316,4 +327,5 @@
     <string name="download_notifier_page_error">A page is missing in directory</string>
     <string name="download_notifier_page_ready_error">A page is not loaded</string>
     <string name="download_notifier_text_only_wifi">No wifi connection available</string>
+
 </resources>

+ 10 - 0
app/src/main/res/values/styles.xml

@@ -121,6 +121,10 @@
         <item name="android:textColor">?attr/colorAccent</item>
     </style>
 
+    <style name="TextAppearance.Medium.Button.Negative">
+        <item name="android:textColor">@color/md_red_500</item>
+    </style>
+
     <!--=======-->
     <!--Widgets-->
     <!--=======-->
@@ -137,6 +141,12 @@
         <item name="layout_behavior">eu.kanade.tachiyomi.widget.FABAnimationUpDown</item>
     </style>
 
+    <style name="Theme.Widget.CardView">
+        <item name="android:layout_marginRight">@dimen/card_margin</item>
+        <item name="android:layout_marginLeft">@dimen/card_margin</item>
+        <item name="android:layout_marginTop">@dimen/card_margin</item>
+    </style>
+
     <style name="Theme.Widget.GridView">
         <item name="android:padding">5dp</item>
         <item name="android:clipToPadding">false</item>