New Recents Page + Download updates

Recent tab is now a combination of recent reads and updates, filtered by unread and an option to go to both old pages (which will be updated later)
Download button now has an option to start downloading a chapter now, which unpauses downloading and moves it to the top of the queue
If a chapter is added to a paused download queue with the notification swiped away, the notification returns
This commit is contained in:
Jay 2020-03-28 01:23:22 -04:00
parent 719aa751b8
commit 6a7762212b
38 changed files with 1414 additions and 164 deletions

View File

@ -6,10 +6,12 @@ import eu.kanade.tachiyomi.data.database.DbProvider
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaChapter import eu.kanade.tachiyomi.data.database.models.MangaChapter
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
import eu.kanade.tachiyomi.data.database.resolvers.ChapterBackupPutResolver import eu.kanade.tachiyomi.data.database.resolvers.ChapterBackupPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.ChapterProgressPutResolver import eu.kanade.tachiyomi.data.database.resolvers.ChapterProgressPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.ChapterSourceOrderPutResolver import eu.kanade.tachiyomi.data.database.resolvers.ChapterSourceOrderPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterGetResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterGetResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterHistoryGetResolver
import eu.kanade.tachiyomi.data.database.tables.ChapterTable import eu.kanade.tachiyomi.data.database.tables.ChapterTable
import java.util.Date import java.util.Date
@ -34,6 +36,16 @@ interface ChapterQueries : DbProvider {
.withGetResolver(MangaChapterGetResolver.INSTANCE) .withGetResolver(MangaChapterGetResolver.INSTANCE)
.prepare() .prepare()
fun getUpdatedManga(date: Date, search: String = "") = db.get()
.listOfObjects(MangaChapterHistory::class.java)
.withQuery(RawQuery.builder()
.query(getRecentsQueryDistinct(search))
.args(date.time)
.observesTables(ChapterTable.TABLE)
.build())
.withGetResolver(MangaChapterHistoryGetResolver.INSTANCE)
.prepare()
fun getChapter(id: Long) = db.get() fun getChapter(id: Long) = db.get()
.`object`(Chapter::class.java) .`object`(Chapter::class.java)
.withQuery(Query.builder() .withQuery(Query.builder()

View File

@ -48,6 +48,21 @@ interface HistoryQueries : DbProvider {
.withGetResolver(MangaChapterHistoryGetResolver.INSTANCE) .withGetResolver(MangaChapterHistoryGetResolver.INSTANCE)
.prepare() .prepare()
/**
* Returns history of recent manga containing last read chapter in 25s
* @param date recent date range
* @offset offset the db by
*/
fun getRecentsWithUnread(date: Date, search: String = "") = db.get()
.listOfObjects(MangaChapterHistory::class.java)
.withQuery(RawQuery.builder()
.query(getRecentReadWithUnreadChapters(search))
.args(date.time)
.observesTables(HistoryTable.TABLE)
.build())
.withGetResolver(MangaChapterHistoryGetResolver.INSTANCE)
.prepare()
fun getHistoryByMangaId(mangaId: Long) = db.get() fun getHistoryByMangaId(mangaId: Long) = db.get()
.listOfObjects(History::class.java) .listOfObjects(History::class.java)
.withQuery(RawQuery.builder() .withQuery(RawQuery.builder()

View File

@ -61,6 +61,29 @@ fun getRecentsQuery() = """
ORDER BY ${Chapter.COL_DATE_UPLOAD} DESC ORDER BY ${Chapter.COL_DATE_UPLOAD} DESC
""" """
/**
* Query to get the recent chapters of manga from the library up to a date.
*/
fun getRecentsQueryDistinct(search: String) = """
SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, ${Manga.TABLE}.*, ${Chapter.TABLE}.*
FROM ${Manga.TABLE}
JOIN ${Chapter.TABLE}
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
JOIN (
SELECT ${Chapter.TABLE}.${Chapter.COL_MANGA_ID},${Chapter.TABLE}.${Chapter.COL_ID} as ${History.COL_CHAPTER_ID},MAX(${Chapter.TABLE}.${Chapter.COL_DATE_UPLOAD})
FROM ${Chapter.TABLE} JOIN ${Manga.TABLE}
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
WHERE ${Chapter.COL_DATE_FETCH} > ?
AND ${Chapter.COL_READ} = 0
GROUP BY ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}) AS newest_chapter
ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = newest_chapter.${Chapter.COL_MANGA_ID}
WHERE ${Manga.COL_FAVORITE} = 1
AND newest_chapter.${History.COL_CHAPTER_ID} = ${Chapter.TABLE}.${Chapter.COL_ID}
AND lower(${Manga.COL_TITLE}) LIKE '%$search%'
ORDER BY ${Chapter.COL_DATE_UPLOAD} DESC
LIMIT 8
"""
/** /**
* Query to get the recently read chapters of manga from the library up to a date. * 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 max_last_read table contains the most recent chapters grouped by manga
@ -114,6 +137,45 @@ fun getRecentMangasLimitQuery(limit: Int = 25, search: String = "") = """
LIMIT $limit LIMIT $limit
""" """
/**
* 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
*/
fun getRecentReadWithUnreadChapters(search: String = "") = """
SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, ${Manga.TABLE}.*, ${Chapter.TABLE}.*, ${History.TABLE}.*
FROM (
SELECT ${Manga.TABLE}.*
FROM ${Manga.TABLE}
LEFT JOIN (
SELECT ${Chapter.COL_MANGA_ID}, COUNT(*) AS unread
FROM ${Chapter.TABLE}
WHERE ${Chapter.COL_READ} = 0
GROUP BY ${Chapter.COL_MANGA_ID}
) AS C
ON ${Manga.COL_ID} = C.${Chapter.COL_MANGA_ID}
WHERE C.unread > 0
GROUP BY ${Manga.COL_ID}
ORDER BY ${Manga.COL_TITLE}
) AS ${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}
AND lower(${Manga.TABLE}.${Manga.COL_TITLE}) LIKE '%$search%'
ORDER BY max_last_read.${History.COL_LAST_READ} DESC
LIMIT 8
"""
fun getHistoryByMangaId() = """ fun getHistoryByMangaId() = """
SELECT ${History.TABLE}.* SELECT ${History.TABLE}.*
FROM ${History.TABLE} FROM ${History.TABLE}

View File

@ -5,6 +5,7 @@ import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
import eu.kanade.tachiyomi.data.database.mappers.ChapterGetResolver import eu.kanade.tachiyomi.data.database.mappers.ChapterGetResolver
import eu.kanade.tachiyomi.data.database.mappers.HistoryGetResolver import eu.kanade.tachiyomi.data.database.mappers.HistoryGetResolver
import eu.kanade.tachiyomi.data.database.mappers.MangaGetResolver import eu.kanade.tachiyomi.data.database.mappers.MangaGetResolver
import eu.kanade.tachiyomi.data.database.models.HistoryImpl
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
class MangaChapterHistoryGetResolver : DefaultGetResolver<MangaChapterHistory>() { class MangaChapterHistoryGetResolver : DefaultGetResolver<MangaChapterHistory>() {
@ -38,11 +39,12 @@ class MangaChapterHistoryGetResolver : DefaultGetResolver<MangaChapterHistory>()
val chapter = chapterResolver.mapFromCursor(cursor) val chapter = chapterResolver.mapFromCursor(cursor)
// Get history object // Get history object
val history = historyGetResolver.mapFromCursor(cursor) val history = try { historyGetResolver.mapFromCursor(cursor) } catch (e: Exception) { HistoryImpl() }
// Make certain column conflicts are dealt with // Make certain column conflicts are dealt with
manga.id = chapter.manga_id manga.id = chapter.manga_id
manga.url = cursor.getString(cursor.getColumnIndex("mangaUrl")) manga.url = cursor.getString(cursor.getColumnIndex("mangaUrl"))
if (history.id != null)
chapter.id = history.chapter_id chapter.id = history.chapter_id
// Return result // Return result

View File

@ -99,6 +99,20 @@ class DownloadManager(val context: Context) {
DownloadService.callListeners(false) DownloadService.callListeners(false)
} }
fun startDownloadNow(chapter: Chapter) {
val download = downloader.queue.find { it.chapter.id == chapter.id } ?: return
val queue = downloader.queue.toMutableList()
queue.remove(download)
queue.add(0, download)
reorderQueue(queue)
if (isPaused()) {
if (DownloadService.isRunning(context))
downloader.start()
else
DownloadService.start(context)
}
}
/** /**
* Reorders the download queue. * Reorders the download queue.
* *

View File

@ -263,6 +263,8 @@ class Downloader(
// Start downloader if needed // Start downloader if needed
if (autoStart && wasEmpty) { if (autoStart && wasEmpty) {
DownloadService.start(this@Downloader.context) DownloadService.start(this@Downloader.context)
} else if (!isRunning) {
notifier.onDownloadPaused()
} }
} }
} }

View File

@ -19,7 +19,6 @@ import android.view.ViewGroup
import android.view.WindowInsets import android.view.WindowInsets
import android.view.WindowManager import android.view.WindowManager
import android.webkit.WebView import android.webkit.WebView
import androidx.appcompat.content.res.AppCompatResources
import androidx.appcompat.graphics.drawable.DrawerArrowDrawable import androidx.appcompat.graphics.drawable.DrawerArrowDrawable
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.graphics.ColorUtils import androidx.core.graphics.ColorUtils
@ -53,6 +52,7 @@ import eu.kanade.tachiyomi.ui.library.LibraryListController
import eu.kanade.tachiyomi.ui.manga.MangaDetailsController import eu.kanade.tachiyomi.ui.manga.MangaDetailsController
import eu.kanade.tachiyomi.ui.recent_updates.RecentChaptersController import eu.kanade.tachiyomi.ui.recent_updates.RecentChaptersController
import eu.kanade.tachiyomi.ui.recently_read.RecentlyReadController import eu.kanade.tachiyomi.ui.recently_read.RecentlyReadController
import eu.kanade.tachiyomi.ui.recents.RecentsController
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
import eu.kanade.tachiyomi.ui.setting.SettingsController import eu.kanade.tachiyomi.ui.setting.SettingsController
import eu.kanade.tachiyomi.ui.setting.SettingsMainController import eu.kanade.tachiyomi.ui.setting.SettingsMainController
@ -61,6 +61,9 @@ import eu.kanade.tachiyomi.util.system.launchUI
import eu.kanade.tachiyomi.util.view.doOnApplyWindowInsets import eu.kanade.tachiyomi.util.view.doOnApplyWindowInsets
import eu.kanade.tachiyomi.util.view.updateLayoutParams import eu.kanade.tachiyomi.util.view.updateLayoutParams
import eu.kanade.tachiyomi.util.view.updatePadding import eu.kanade.tachiyomi.util.view.updatePadding
import java.util.Date
import java.util.concurrent.TimeUnit
import kotlin.math.abs
import kotlinx.android.synthetic.main.main_activity.* import kotlinx.android.synthetic.main.main_activity.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
@ -68,9 +71,6 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.util.Date
import java.util.concurrent.TimeUnit
import kotlin.math.abs
open class MainActivity : BaseActivity(), DownloadServiceListener { open class MainActivity : BaseActivity(), DownloadServiceListener {
@ -151,24 +151,24 @@ open class MainActivity : BaseActivity(), DownloadServiceListener {
else LibraryController(), id else LibraryController(), id
) )
R.id.nav_recents -> { R.id.nav_recents -> {
if (preferences.showRecentUpdates().getOrDefault()) setRoot( setRoot(RecentsController(), id)
RecentChaptersController(), // if (preferences.showRecentUpdates().getOrDefault()) setRoot(
id // RecentChaptersController(), id
) // )
else setRoot(RecentlyReadController(), id) // else setRoot(RecentlyReadController(), id)
} }
R.id.nav_catalogues -> setRoot(CatalogueController(), id) R.id.nav_catalogues -> setRoot(CatalogueController(), id)
} }
} else if (currentRoot.tag()?.toIntOrNull() == id) { } else if (currentRoot.tag()?.toIntOrNull() == id) {
if (router.backstackSize == 1) { if (router.backstackSize == 1) {
when (id) { when (id) {
R.id.nav_recents -> { /*R.id.nav_recents -> {
val showRecents = preferences.showRecentUpdates().getOrDefault() val showRecents = preferences.showRecentUpdates().getOrDefault()
if (!showRecents) setRoot(RecentChaptersController(), id) if (!showRecents) setRoot(RecentChaptersController(), id)
else setRoot(RecentlyReadController(), id) else setRoot(RecentlyReadController(), id)
preferences.showRecentUpdates().set(!showRecents) preferences.showRecentUpdates().set(!showRecents)
updateRecentsIcon() updateRecentsIcon()
} }*/
R.id.nav_library -> { R.id.nav_library -> {
val controller = val controller =
router.getControllerWithTag(id.toString()) as? LibraryController router.getControllerWithTag(id.toString()) as? LibraryController
@ -192,7 +192,7 @@ open class MainActivity : BaseActivity(), DownloadServiceListener {
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
container.systemUiVisibility = container.systemUiVisibility =
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
updateRecentsIcon() // updateRecentsIcon()
supportActionBar?.setDisplayShowCustomEnabled(true) supportActionBar?.setDisplayShowCustomEnabled(true)
@ -309,7 +309,7 @@ open class MainActivity : BaseActivity(), DownloadServiceListener {
} }
} }
fun updateRecentsIcon() { /*fun updateRecentsIcon() {
bottom_nav.menu.findItem(R.id.nav_recents).icon = AppCompatResources.getDrawable( bottom_nav.menu.findItem(R.id.nav_recents).icon = AppCompatResources.getDrawable(
this, this,
if (preferences.showRecentUpdates() if (preferences.showRecentUpdates()
@ -317,7 +317,7 @@ open class MainActivity : BaseActivity(), DownloadServiceListener {
) R.drawable.recent_updates_selector_24dp ) R.drawable.recent_updates_selector_24dp
else R.drawable.recent_read_selector_24dp else R.drawable.recent_read_selector_24dp
) )
} }*/
override fun startSupportActionMode(callback: androidx.appcompat.view.ActionMode.Callback): androidx.appcompat.view.ActionMode? { override fun startSupportActionMode(callback: androidx.appcompat.view.ActionMode.Callback): androidx.appcompat.view.ActionMode? {
window?.statusBarColor = getResourceColor(R.attr.colorPrimaryVariant) window?.statusBarColor = getResourceColor(R.attr.colorPrimaryVariant)
@ -387,9 +387,14 @@ open class MainActivity : BaseActivity(), DownloadServiceListener {
when (intent.action) { when (intent.action) {
SHORTCUT_LIBRARY -> bottom_nav.selectedItemId = R.id.nav_library SHORTCUT_LIBRARY -> bottom_nav.selectedItemId = R.id.nav_library
SHORTCUT_RECENTLY_UPDATED, SHORTCUT_RECENTLY_READ -> { SHORTCUT_RECENTLY_UPDATED, SHORTCUT_RECENTLY_READ -> {
preferences.showRecentUpdates().set(intent.action == SHORTCUT_RECENTLY_UPDATED) // preferences.showRecentUpdates().set(intent.action == SHORTCUT_RECENTLY_UPDATED)
bottom_nav.selectedItemId = R.id.nav_recents bottom_nav.selectedItemId = R.id.nav_recents
updateRecentsIcon() val controller: Controller = when (intent.action) {
SHORTCUT_RECENTLY_UPDATED -> RecentChaptersController()
else -> RecentlyReadController()
}
router.pushController(controller.withFadeTransaction())
// updateRecentsIcon()
} }
SHORTCUT_CATALOGUES -> bottom_nav.selectedItemId = R.id.nav_catalogues SHORTCUT_CATALOGUES -> bottom_nav.selectedItemId = R.id.nav_catalogues
SHORTCUT_EXTENSIONS -> { SHORTCUT_EXTENSIONS -> {

View File

@ -24,7 +24,7 @@ class MangaDetailsAdapter(
var items: List<ChapterItem> = emptyList() var items: List<ChapterItem> = emptyList()
val coverListener: MangaHeaderInterface = controller val delegate: MangaDetailsInterface = controller
val readColor = context.getResourceColor(android.R.attr.textColorHint) val readColor = context.getResourceColor(android.R.attr.textColorHint)
@ -67,13 +67,15 @@ class MangaDetailsAdapter(
} }
} }
interface MangaDetailsInterface : MangaHeaderInterface, DownloadInterface
interface MangaHeaderInterface { interface MangaHeaderInterface {
fun coverColor(): Int? fun coverColor(): Int?
fun mangaPresenter(): MangaDetailsPresenter fun mangaPresenter(): MangaDetailsPresenter
fun prepareToShareManga() fun prepareToShareManga()
fun openInWebView() fun openInWebView()
fun startDownloadRange(position: Int)
fun readNextChapter() fun readNextChapter()
fun downloadChapter(position: Int)
fun topCoverHeight(): Int fun topCoverHeight(): Int
fun tagClicked(text: String) fun tagClicked(text: String)
fun showChapterFilter() fun showChapterFilter()
@ -81,6 +83,10 @@ class MangaDetailsAdapter(
fun copyToClipboard(content: String, label: Int) fun copyToClipboard(content: String, label: Int)
fun zoomImageFromThumb(thumbView: View) fun zoomImageFromThumb(thumbView: View)
fun showTrackingSheet() fun showTrackingSheet()
fun startDownloadRange(position: Int) }
interface DownloadInterface {
fun downloadChapter(position: Int)
fun startDownloadNow(position: Int)
} }
} }

View File

@ -115,7 +115,7 @@ class MangaDetailsController : BaseController,
FlexibleAdapter.OnItemClickListener, FlexibleAdapter.OnItemClickListener,
FlexibleAdapter.OnItemLongClickListener, FlexibleAdapter.OnItemLongClickListener,
ActionMode.Callback, ActionMode.Callback,
MangaDetailsAdapter.MangaHeaderInterface, MangaDetailsAdapter.MangaDetailsInterface,
FlexibleAdapter.OnItemMoveListener, FlexibleAdapter.OnItemMoveListener,
ChangeMangaCategoriesDialog.Listener { ChangeMangaCategoriesDialog.Listener {
@ -577,7 +577,7 @@ class MangaDetailsController : BaseController,
setOnQueryTextChangeListener(searchView) { setOnQueryTextChangeListener(searchView) {
query = it ?: "" query = it ?: ""
if (query.isNotEmpty()) { if (query.isNotEmpty()) {
(recycler.findViewHolderForAdapterPosition(0) as? MangaHeaderHolder)?.collaspe() (recycler.findViewHolderForAdapterPosition(0) as? MangaHeaderHolder)?.collapse()
} else (recycler.findViewHolderForAdapterPosition(0) as? MangaHeaderHolder)?.expand() } else (recycler.findViewHolderForAdapterPosition(0) as? MangaHeaderHolder)?.expand()
adapter?.setFilter(query) adapter?.setFilter(query)
@ -717,6 +717,11 @@ class MangaDetailsController : BaseController,
} }
} }
override fun startDownloadNow(position: Int) {
val chapter = (adapter?.getItem(position) as? ChapterItem) ?: return
presenter.startDownloadingNow(chapter)
}
private fun downloadChapters(chapters: List<ChapterItem>) { private fun downloadChapters(chapters: List<ChapterItem>) {
val view = view val view = view
presenter.downloadChapters(chapters) presenter.downloadChapters(chapters)

View File

@ -136,6 +136,9 @@ class MangaDetailsPresenter(
for (chapter in chapters) { for (chapter in chapters) {
if (downloadManager.isChapterDownloaded(chapter, manga)) { if (downloadManager.isChapterDownloaded(chapter, manga)) {
chapter.status = Download.DOWNLOADED chapter.status = Download.DOWNLOADED
} else if (downloadManager.hasQueue()) {
chapter.status = downloadManager.queue.find { it.chapter.id == chapter.id }
?.status ?: 0
} }
} }
} }
@ -276,6 +279,10 @@ class MangaDetailsPresenter(
return chapters.maxBy { it.chapter_number }?.chapter_number return chapters.maxBy { it.chapter_number }?.chapter_number
} }
fun startDownloadingNow(chapter: Chapter) {
downloadManager.startDownloadNow(chapter)
}
/** /**
* Downloads the given list of chapters with the manager. * Downloads the given list of chapters with the manager.
* @param chapters the list of chapters to download. * @param chapters the list of chapters to download.

View File

@ -34,9 +34,9 @@ class MangaHeaderHolder(
) : BaseFlexibleViewHolder(view, adapter) { ) : BaseFlexibleViewHolder(view, adapter) {
init { init {
start_reading_button.setOnClickListener { adapter.coverListener.readNextChapter() } start_reading_button.setOnClickListener { adapter.delegate.readNextChapter() }
top_view.updateLayoutParams<ConstraintLayout.LayoutParams> { top_view.updateLayoutParams<ConstraintLayout.LayoutParams> {
height = adapter.coverListener.topCoverHeight() height = adapter.delegate.topCoverHeight()
} }
more_button.setOnClickListener { expandDesc() } more_button.setOnClickListener { expandDesc() }
manga_summary.setOnClickListener { expandDesc() } manga_summary.setOnClickListener { expandDesc() }
@ -47,30 +47,30 @@ class MangaHeaderHolder(
more_button_group.visible() more_button_group.visible()
} }
manga_genres_tags.setOnTagClickListener { manga_genres_tags.setOnTagClickListener {
adapter.coverListener.tagClicked(it) adapter.delegate.tagClicked(it)
} }
filter_button.setOnClickListener { adapter.coverListener.showChapterFilter() } filter_button.setOnClickListener { adapter.delegate.showChapterFilter() }
filters_text.setOnClickListener { adapter.coverListener.showChapterFilter() } filters_text.setOnClickListener { adapter.delegate.showChapterFilter() }
chapters_title.setOnClickListener { adapter.coverListener.showChapterFilter() } chapters_title.setOnClickListener { adapter.delegate.showChapterFilter() }
webview_button.setOnClickListener { adapter.coverListener.openInWebView() } webview_button.setOnClickListener { adapter.delegate.openInWebView() }
share_button.setOnClickListener { adapter.coverListener.prepareToShareManga() } share_button.setOnClickListener { adapter.delegate.prepareToShareManga() }
favorite_button.setOnClickListener { favorite_button.setOnClickListener {
adapter.coverListener.favoriteManga(false) adapter.delegate.favoriteManga(false)
} }
favorite_button.setOnLongClickListener { favorite_button.setOnLongClickListener {
adapter.coverListener.favoriteManga(true) adapter.delegate.favoriteManga(true)
true true
} }
manga_full_title.setOnLongClickListener { manga_full_title.setOnLongClickListener {
adapter.coverListener.copyToClipboard(manga_full_title.text.toString(), R.string.manga_info_full_title_label) adapter.delegate.copyToClipboard(manga_full_title.text.toString(), R.string.manga_info_full_title_label)
true true
} }
manga_author.setOnLongClickListener { manga_author.setOnLongClickListener {
adapter.coverListener.copyToClipboard(manga_author.text.toString(), R.string.manga_info_author_label) adapter.delegate.copyToClipboard(manga_author.text.toString(), R.string.manga_info_author_label)
true true
} }
manga_cover.setOnClickListener { adapter.coverListener.zoomImageFromThumb(cover_card) } manga_cover.setOnClickListener { adapter.delegate.zoomImageFromThumb(cover_card) }
track_button.setOnClickListener { adapter.coverListener.showTrackingSheet() } track_button.setOnClickListener { adapter.delegate.showTrackingSheet() }
if (startExpanded) if (startExpanded)
expandDesc() expandDesc()
} }
@ -86,7 +86,7 @@ class MangaHeaderHolder(
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
fun bind(item: MangaHeaderItem, manga: Manga) { fun bind(item: MangaHeaderItem, manga: Manga) {
val presenter = adapter.coverListener.mangaPresenter() val presenter = adapter.delegate.mangaPresenter()
manga_full_title.text = manga.title manga_full_title.text = manga.title
if (manga.genre.isNullOrBlank().not()) if (manga.genre.isNullOrBlank().not())
@ -109,7 +109,7 @@ class MangaHeaderHolder(
more_button_group.gone() more_button_group.gone()
} else more_button_group.visible() } else more_button_group.visible()
} }
if (adapter.hasFilter()) collaspe() if (adapter.hasFilter()) collapse()
else expand() else expand()
} }
manga_summary_label.text = itemView.context.getString(R.string.about_this, manga_summary_label.text = itemView.context.getString(R.string.about_this,
@ -139,7 +139,7 @@ class MangaHeaderHolder(
) )
checked(!item.isLocked && manga.favorite) checked(!item.isLocked && manga.favorite)
} }
true_backdrop.setBackgroundColor(adapter.coverListener.coverColor() true_backdrop.setBackgroundColor(adapter.delegate.coverColor()
?: itemView.context.getResourceColor(android.R.attr.colorBackground)) ?: itemView.context.getResourceColor(android.R.attr.colorBackground))
val tracked = presenter.isTracked() && !item.isLocked val tracked = presenter.isTracked() && !item.isLocked
@ -180,7 +180,7 @@ class MangaHeaderHolder(
chapters_title.text = itemView.resources.getQuantityString(R.plurals.chapters, count, count) chapters_title.text = itemView.resources.getQuantityString(R.plurals.chapters, count, count)
top_view.updateLayoutParams<ConstraintLayout.LayoutParams> { top_view.updateLayoutParams<ConstraintLayout.LayoutParams> {
height = adapter.coverListener.topCoverHeight() height = adapter.delegate.topCoverHeight()
} }
manga_status.text = (itemView.context.getString(when (manga.status) { manga_status.text = (itemView.context.getString(when (manga.status) {
@ -243,7 +243,7 @@ class MangaHeaderHolder(
} }
fun updateTracking() { fun updateTracking() {
val presenter = adapter.coverListener.mangaPresenter() ?: return val presenter = adapter.delegate.mangaPresenter() ?: return
val tracked = presenter.isTracked() val tracked = presenter.isTracked()
with(track_button) { with(track_button) {
text = itemView.context.getString(if (tracked) R.string.action_filter_tracked text = itemView.context.getString(if (tracked) R.string.action_filter_tracked
@ -255,7 +255,7 @@ class MangaHeaderHolder(
} }
} }
fun collaspe() { fun collapse() {
sub_item_group.gone() sub_item_group.gone()
if (more_button.visibility == View.VISIBLE || more_button.visibility == View.INVISIBLE) if (more_button.visibility == View.VISIBLE || more_button.visibility == View.INVISIBLE)
more_button_group.invisible() more_button_group.invisible()

View File

@ -25,7 +25,7 @@ class ChapterHolder(
init { init {
download_button.setOnClickListener { downloadOrRemoveMenu() } download_button.setOnClickListener { downloadOrRemoveMenu() }
download_button.setOnLongClickListener { download_button.setOnLongClickListener {
adapter.coverListener.startDownloadRange(adapterPosition) adapter.delegate.startDownloadRange(adapterPosition)
true true
} }
} }
@ -33,7 +33,7 @@ class ChapterHolder(
private fun downloadOrRemoveMenu() { private fun downloadOrRemoveMenu() {
val chapter = adapter.getItem(adapterPosition) as? ChapterItem ?: return val chapter = adapter.getItem(adapterPosition) as? ChapterItem ?: return
if (chapter.status == Download.NOT_DOWNLOADED || chapter.status == Download.ERROR) { if (chapter.status == Download.NOT_DOWNLOADED || chapter.status == Download.ERROR) {
adapter.coverListener.downloadChapter(adapterPosition) adapter.delegate.downloadChapter(adapterPosition)
} else { } else {
download_button.post { download_button.post {
// Create a PopupMenu, giving it the clicked view for an anchor // Create a PopupMenu, giving it the clicked view for an anchor
@ -42,14 +42,19 @@ class ChapterHolder(
// Inflate our menu resource into the PopupMenu's Menu // Inflate our menu resource into the PopupMenu's Menu
popup.menuInflater.inflate(R.menu.chapter_download, popup.menu) popup.menuInflater.inflate(R.menu.chapter_download, popup.menu)
popup.menu.findItem(R.id.action_start).isVisible = chapter.status == Download.QUEUE
// Hide download and show delete if the chapter is downloaded // Hide download and show delete if the chapter is downloaded
if (chapter.status != Download.DOWNLOADED) popup.menu.findItem(R.id.action_delete).title = download_button.context.getString( if (chapter.status != Download.DOWNLOADED) popup.menu.findItem(R.id.action_delete).title = download_button.context.getString(
R.string.action_cancel R.string.action_cancel
) )
// Set a listener so we are notified if a menu item is clicked // Set a listener so we are notified if a menu item is clicked
popup.setOnMenuItemClickListener { _ -> popup.setOnMenuItemClickListener { item ->
adapter.coverListener.downloadChapter(adapterPosition) when (item.itemId) {
R.id.action_delete -> adapter.delegate.downloadChapter(adapterPosition)
R.id.action_start -> adapter.delegate.startDownloadNow(adapterPosition)
}
true true
} }

View File

@ -8,7 +8,6 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.SearchView
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.jakewharton.rxbinding.support.v4.widget.refreshes import com.jakewharton.rxbinding.support.v4.widget.refreshes
@ -19,24 +18,17 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.library.LibraryUpdateService import eu.kanade.tachiyomi.data.library.LibraryUpdateService
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.main.RootSearchInterface
import eu.kanade.tachiyomi.ui.manga.MangaDetailsController import eu.kanade.tachiyomi.ui.manga.MangaDetailsController
import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.ui.recently_read.RecentlyReadController
import eu.kanade.tachiyomi.util.system.notificationManager import eu.kanade.tachiyomi.util.system.notificationManager
import eu.kanade.tachiyomi.util.view.applyWindowInsetsForRootController
import eu.kanade.tachiyomi.util.view.scrollViewWith import eu.kanade.tachiyomi.util.view.scrollViewWith
import eu.kanade.tachiyomi.util.view.setOnQueryTextChangeListener
import eu.kanade.tachiyomi.util.view.snack import eu.kanade.tachiyomi.util.view.snack
import kotlinx.android.synthetic.main.main_activity.* import kotlinx.android.synthetic.main.main_activity.*
import kotlinx.android.synthetic.main.recent_chapters_controller.* import kotlinx.android.synthetic.main.recent_chapters_controller.*
import timber.log.Timber import timber.log.Timber
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
/** /**
* Fragment that shows recent chapters. * Fragment that shows recent chapters.
@ -49,7 +41,6 @@ class RecentChaptersController : NucleusController<RecentChaptersPresenter>(),
FlexibleAdapter.OnItemLongClickListener, FlexibleAdapter.OnItemLongClickListener,
FlexibleAdapter.OnUpdateListener, FlexibleAdapter.OnUpdateListener,
ConfirmDeleteChaptersDialog.Listener, ConfirmDeleteChaptersDialog.Listener,
RootSearchInterface,
RecentChaptersAdapter.OnCoverClickListener { RecentChaptersAdapter.OnCoverClickListener {
init { init {
@ -86,7 +77,7 @@ class RecentChaptersController : NucleusController<RecentChaptersPresenter>(),
*/ */
override fun onViewCreated(view: View) { override fun onViewCreated(view: View) {
super.onViewCreated(view) super.onViewCreated(view)
view.applyWindowInsetsForRootController(activity!!.bottom_nav) // view.applyWindowInsetsForController()
view.context.notificationManager.cancel(Notifications.ID_NEW_CHAPTERS) view.context.notificationManager.cancel(Notifications.ID_NEW_CHAPTERS)
// Init RecyclerView and adapter // Init RecyclerView and adapter
@ -347,43 +338,15 @@ class RecentChaptersController : NucleusController<RecentChaptersPresenter>(),
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.recent_updates, menu) inflater.inflate(R.menu.recent_updates, menu)
val searchItem = menu.findItem(R.id.action_search)
val searchView = searchItem.actionView as SearchView
searchView.queryHint = resources?.getString(R.string.action_search)
if (query.isNotEmpty()) {
searchItem.expandActionView()
searchView.setQuery(query, true)
searchView.clearFocus()
}
setOnQueryTextChangeListener(searchView) {
if (query != it) {
query = it ?: return@setOnQueryTextChangeListener false
adapter?.setFilter(query)
adapter?.performFilter()
}
true
}
// Fixes problem with the overflow icon showing up in lieu of search
searchItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
return true
}
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
activity?.invalidateOptionsMenu()
return true
}
})
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.action_recents -> { R.id.action_sort -> {
router.setRoot( /*router.setRoot(
RecentlyReadController().withFadeTransaction().tag(R.id.nav_recents.toString())) RecentlyReadController().withFadeTransaction().tag(R.id.nav_recents.toString()))
Injekt.get<PreferencesHelper>().showRecentUpdates().set(false) Injekt.get<PreferencesHelper>().showRecentUpdates().set(false)
(activity as? MainActivity)?.updateRecentsIcon() (activity as? MainActivity)?.updateRecentsIcon()*/
} }
} }
return super.onOptionsItemSelected(item) return super.onOptionsItemSelected(item)

View File

@ -19,7 +19,7 @@ import uy.kohesive.injekt.injectLazy
* @constructor creates an instance of the adapter. * @constructor creates an instance of the adapter.
*/ */
class RecentlyReadAdapter(controller: RecentlyReadController) : class RecentlyReadAdapter(controller: RecentlyReadController) :
FlexibleAdapter<IFlexible<*>>(null, controller, true) { FlexibleAdapter<IFlexible<*>>(null, controller, true) {
val sourceManager by injectLazy<SourceManager>() val sourceManager by injectLazy<SourceManager>()

View File

@ -15,25 +15,17 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.BackupRestoreService import eu.kanade.tachiyomi.data.backup.BackupRestoreService
import eu.kanade.tachiyomi.data.database.models.History import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.base.controller.BaseController import eu.kanade.tachiyomi.ui.base.controller.BaseController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.catalogue.browse.ProgressItem import eu.kanade.tachiyomi.ui.catalogue.browse.ProgressItem
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.main.RootSearchInterface
import eu.kanade.tachiyomi.ui.manga.MangaDetailsController import eu.kanade.tachiyomi.ui.manga.MangaDetailsController
import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.ui.recent_updates.RecentChaptersController
import eu.kanade.tachiyomi.util.system.launchUI import eu.kanade.tachiyomi.util.system.launchUI
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.view.RecyclerWindowInsetsListener import eu.kanade.tachiyomi.util.view.RecyclerWindowInsetsListener
import eu.kanade.tachiyomi.util.view.applyWindowInsetsForRootController
import eu.kanade.tachiyomi.util.view.scrollViewWith import eu.kanade.tachiyomi.util.view.scrollViewWith
import eu.kanade.tachiyomi.util.view.setOnQueryTextChangeListener import eu.kanade.tachiyomi.util.view.setOnQueryTextChangeListener
import kotlinx.android.synthetic.main.main_activity.*
import kotlinx.android.synthetic.main.recently_read_controller.* import kotlinx.android.synthetic.main.recently_read_controller.*
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
/** /**
* Fragment that shows recently read manga. * Fragment that shows recently read manga.
@ -46,7 +38,6 @@ class RecentlyReadController(bundle: Bundle? = null) : BaseController(bundle),
RecentlyReadAdapter.OnRemoveClickListener, RecentlyReadAdapter.OnRemoveClickListener,
RecentlyReadAdapter.OnResumeClickListener, RecentlyReadAdapter.OnResumeClickListener,
RecentlyReadAdapter.OnCoverClickListener, RecentlyReadAdapter.OnCoverClickListener,
RootSearchInterface,
RemoveHistoryDialog.Listener { RemoveHistoryDialog.Listener {
init { init {
@ -69,7 +60,7 @@ class RecentlyReadController(bundle: Bundle? = null) : BaseController(bundle),
private var recentItems: MutableList<RecentlyReadItem>? = null private var recentItems: MutableList<RecentlyReadItem>? = null
override fun getTitle(): String? { override fun getTitle(): String? {
return resources?.getString(R.string.label_recent_manga) return resources?.getString(R.string.history)
} }
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
@ -83,7 +74,7 @@ class RecentlyReadController(bundle: Bundle? = null) : BaseController(bundle),
*/ */
override fun onViewCreated(view: View) { override fun onViewCreated(view: View) {
super.onViewCreated(view) super.onViewCreated(view)
view.applyWindowInsetsForRootController(activity!!.bottom_nav) // view.applyWindowInsetsForController()
// Initialize adapter // Initialize adapter
adapter = RecentlyReadAdapter(this) adapter = RecentlyReadAdapter(this)
recycler.adapter = adapter recycler.adapter = adapter
@ -232,7 +223,7 @@ class RecentlyReadController(bundle: Bundle? = null) : BaseController(bundle),
}) })
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean { /*override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.action_recents -> { R.id.action_recents -> {
router.setRoot( router.setRoot(
@ -242,5 +233,5 @@ class RecentlyReadController(bundle: Bundle? = null) : BaseController(bundle),
} }
} }
return super.onOptionsItemSelected(item) return super.onOptionsItemSelected(item)
} }*/
} }

View File

@ -4,6 +4,7 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.History import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.util.system.executeOnIO
import eu.kanade.tachiyomi.util.system.launchUI import eu.kanade.tachiyomi.util.system.launchUI
import java.util.Calendar import java.util.Calendar
import java.util.Comparator import java.util.Comparator
@ -36,13 +37,13 @@ class RecentlyReadPresenter(private val view: RecentlyReadController) {
* Get all recent manga up to a point * Get all recent manga up to a point
* @return list of history * @return list of history
*/ */
private fun getRecentMangaLimit(search: String = ""): List<RecentlyReadItem> { private suspend fun getRecentMangaLimit(search: String = ""): List<RecentlyReadItem> {
// Set date for recent manga // Set date for recent manga
val cal = Calendar.getInstance() val cal = Calendar.getInstance()
cal.time = Date() cal.time = Date()
cal.add(Calendar.YEAR, -50) cal.add(Calendar.YEAR, -50)
return db.getRecentMangaLimit(cal.time, lastCount, search).executeAsBlocking() return db.getRecentMangaLimit(cal.time, lastCount, search).executeOnIO()
.map(::RecentlyReadItem) .map(::RecentlyReadItem)
} }
@ -57,7 +58,7 @@ class RecentlyReadPresenter(private val view: RecentlyReadController) {
} }
suspend fun refresh(search: String? = null): List<RecentlyReadItem> { suspend fun refresh(search: String? = null): List<RecentlyReadItem> {
val manga = withContext(Dispatchers.IO) { getRecentMangaLimit(search ?: "") } val manga = getRecentMangaLimit(search ?: "")
checkIfNew(manga.size, search) checkIfNew(manga.size, search)
lastSearch = search ?: lastSearch lastSearch = search ?: lastSearch
lastCount = manga.size lastCount = manga.size

View File

@ -0,0 +1,32 @@
package eu.kanade.tachiyomi.ui.recents
import android.widget.ImageView
import androidx.recyclerview.widget.ItemTouchHelper
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.ui.manga.MangaDetailsAdapter
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
class RecentMangaAdapter(val delegate: RecentsInterface) :
FlexibleAdapter<IFlexible<RecentMangaHolder>>(null, delegate, true) {
val decimalFormat = DecimalFormat("#.###", DecimalFormatSymbols()
.apply { decimalSeparator = '.' })
interface RecentsInterface : RecentMangaInterface, MangaDetailsAdapter.DownloadInterface
interface RecentMangaInterface {
fun onCoverClick(position: Int)
fun markAsRead(position: Int)
fun setCover(manga: Manga, view: ImageView)
}
override fun onItemSwiped(position: Int, direction: Int) {
super.onItemSwiped(position, direction)
when (direction) {
ItemTouchHelper.LEFT -> delegate.markAsRead(position)
}
}
}

View File

@ -0,0 +1,116 @@
package eu.kanade.tachiyomi.ui.recents
import android.text.format.DateUtils
import android.view.View
import androidx.appcompat.widget.PopupMenu
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import eu.kanade.tachiyomi.util.view.visibleIf
import java.util.Date
import kotlinx.android.synthetic.main.download_button.*
import kotlinx.android.synthetic.main.recent_manga_item.*
class RecentMangaHolder(
view: View,
val adapter: RecentMangaAdapter
) : BaseFlexibleViewHolder(view, adapter) {
init {
cover_thumbnail.setOnClickListener { adapter.delegate.onCoverClick(adapterPosition) }
download_button.setOnClickListener { downloadOrRemoveMenu() }
}
private fun downloadOrRemoveMenu() {
val chapter = adapter.getItem(adapterPosition) as? RecentMangaItem ?: return
if (chapter.status == Download.NOT_DOWNLOADED || chapter.status == Download.ERROR) {
adapter.delegate.downloadChapter(adapterPosition)
} else {
download_button.post {
// Create a PopupMenu, giving it the clicked view for an anchor
val popup = PopupMenu(download_button.context, download_button)
// Inflate our menu resource into the PopupMenu's Menu
popup.menuInflater.inflate(R.menu.chapter_download, popup.menu)
popup.menu.findItem(R.id.action_start).isVisible = chapter.status == Download.QUEUE
// Hide download and show delete if the chapter is downloaded
if (chapter.status != Download.DOWNLOADED) popup.menu.findItem(R.id.action_delete).title = download_button.context.getString(
R.string.action_cancel
)
// Set a listener so we are notified if a menu item is clicked
popup.setOnMenuItemClickListener { item ->
when (item.itemId) {
R.id.action_delete -> adapter.delegate.downloadChapter(adapterPosition)
R.id.action_start -> adapter.delegate.startDownloadNow(adapterPosition)
}
true
}
// Finally show the PopupMenu
popup.show()
}
}
}
fun bind(item: RecentMangaItem) {
download_button.visibleIf(item.mch.manga.source != LocalSource.ID)
title.text = item.mch.manga.title
val holder = (adapter.delegate as RecentsHolder)
val isSearch =
(holder.adapter.getItem(holder.adapterPosition) as RecentsItem).recentType == RecentsItem.SEARCH
subtitle.text = item.chapter.name
body.text = if (isSearch) when {
item.chapter.id != item.mch.chapter.id -> body.context.getString(
R.string.last_read_chapter_x, adapter.decimalFormat.format(
item.mch.chapter.chapter_number
) + " (${DateUtils.getRelativeTimeSpanString(
item.mch.history.last_read, Date().time, DateUtils.MINUTE_IN_MILLIS
)})"
)
item.mch.history.id == null -> body.context.getString(
R.string.uploaded_x, DateUtils.getRelativeTimeSpanString(
item.chapter.date_upload, Date().time, DateUtils.HOUR_IN_MILLIS
).toString()
)
else -> body.context.getString(
R.string.last_read_x, DateUtils.getRelativeTimeSpanString(
item.mch.history.last_read, Date().time, DateUtils.MINUTE_IN_MILLIS
).toString()
)
} else when {
item.chapter.id != item.mch.chapter.id -> body.context.getString(
R.string.last_read_chapter_x, adapter.decimalFormat.format(
item.mch.chapter.chapter_number
)
)
item.mch.history.id == null -> DateUtils.getRelativeTimeSpanString(
item.chapter.date_upload, Date().time, DateUtils.HOUR_IN_MILLIS
).toString()
item.chapter.pages_left > 0 -> itemView.resources.getQuantityString(
R.plurals.pages_left, item.chapter.pages_left, item.chapter.pages_left
)
else -> ""
}
adapter.delegate.setCover(item.mch.manga, cover_thumbnail)
notifyStatus(
if (adapter.isSelected(adapterPosition)) Download.CHECKED else item.status,
item.progress
)
}
fun notifyStatus(status: Int, progress: Int) = with(download_button) {
setDownloadStatus(status, progress)
}
override fun getFrontView(): View {
return front_view
}
override fun getRearRightView(): View {
return right_view
}
}

View File

@ -0,0 +1,63 @@
package eu.kanade.tachiyomi.ui.recents
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.source.model.Page
class RecentMangaItem(val mch: MangaChapterHistory, val chapter: Chapter) :
AbstractFlexibleItem<RecentMangaHolder>
() {
private var _status: Int = 0
val progress: Int
get() {
val pages = download?.pages ?: return 0
return pages.map(Page::progress).average().toInt()
}
var status: Int
get() = download?.status ?: _status
set(value) { _status = value }
@Transient var download: Download? = null
override fun getLayoutRes(): Int {
return R.layout.recent_manga_item
}
override fun createViewHolder(
view: View,
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>
): RecentMangaHolder {
return RecentMangaHolder(view, adapter as RecentMangaAdapter)
}
override fun bindViewHolder(
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
holder: RecentMangaHolder,
position: Int,
payloads: MutableList<Any?>?
) {
holder.bind(this)
}
override fun equals(other: Any?): Boolean {
if (other is RecentMangaItem) {
return chapter.id == other.chapter.id
}
return false
}
override fun hashCode(): Int {
return chapter.id!!.hashCode()
}
}

View File

@ -0,0 +1,21 @@
package eu.kanade.tachiyomi.ui.recents
import android.widget.ImageView
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
class RecentsAdapter(val delegate: RecentsInterface) :
FlexibleAdapter<IFlexible<RecentsHolder>>(null, delegate, true) {
interface RecentsInterface {
fun resumeManga(manga: Manga, chapter: Chapter)
fun showManga(manga: Manga)
fun markAsRead(manga: Manga, chapter: Chapter)
fun downloadChapter(item: RecentMangaItem)
fun downloadChapterNow(chapter: Chapter)
fun setCover(manga: Manga, view: ImageView)
fun viewAll(position: Int)
}
}

View File

@ -0,0 +1,256 @@
package eu.kanade.tachiyomi.ui.recents
import android.app.Activity
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.appcompat.widget.SearchView
import androidx.recyclerview.widget.LinearLayoutManager
import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.signature.ObjectKey
import com.google.android.material.snackbar.BaseTransientBottomBar
import com.google.android.material.snackbar.Snackbar
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaImpl
import eu.kanade.tachiyomi.data.download.DownloadService
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
import eu.kanade.tachiyomi.ui.base.controller.BaseController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.main.RootSearchInterface
import eu.kanade.tachiyomi.ui.manga.MangaDetailsController
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.ui.recent_updates.RecentChaptersController
import eu.kanade.tachiyomi.ui.recently_read.RecentlyReadController
import eu.kanade.tachiyomi.util.view.applyWindowInsetsForRootController
import eu.kanade.tachiyomi.util.view.scrollViewWith
import eu.kanade.tachiyomi.util.view.setOnQueryTextChangeListener
import eu.kanade.tachiyomi.util.view.snack
import kotlinx.android.synthetic.main.main_activity.*
import kotlinx.android.synthetic.main.recently_read_controller.*
/**
* Fragment that shows recently read manga.
* Uses R.layout.fragment_recently_read.
* UI related actions should be called from here.
*/
class RecentsController(bundle: Bundle? = null) : BaseController(bundle),
FlexibleAdapter.OnUpdateListener,
RecentsAdapter.RecentsInterface,
RootSearchInterface {
init {
setHasOptionsMenu(true)
}
/**
* Adapter containing the recent manga.
*/
private val adapter = RecentsAdapter(this)
private var presenter = RecentsPresenter(this)
private var recentItems: List<RecentsItem>? = null
private var snack: Snackbar? = null
private var lastChapterId: Long? = null
override fun getTitle(): String? {
return resources?.getString(R.string.short_recents)
}
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
return inflater.inflate(R.layout.recently_read_controller, container, false)
}
/**
* Called when view is created
*
* @param view created view
*/
override fun onViewCreated(view: View) {
super.onViewCreated(view)
view.applyWindowInsetsForRootController(activity!!.bottom_nav)
// Initialize adapter
recycler.adapter = adapter
recycler.layoutManager = LinearLayoutManager(view.context)
recycler.setHasFixedSize(true)
scrollViewWith(recycler, skipFirstSnap = true)
if (recentItems != null) adapter.updateDataSet(recentItems!!.toList())
presenter.onCreate()
}
override fun onActivityResumed(activity: Activity) {
super.onActivityResumed(activity)
if (view != null)
refresh()
}
override fun onDestroy() {
super.onDestroy()
snack?.dismiss()
presenter.onDestroy()
snack = null
}
fun refresh() = presenter.getRecents()
fun showLists(recents: List<RecentsItem>) {
recentItems = recents
adapter.updateDataSet(recents)
if (lastChapterId != null) {
refreshItem(lastChapterId ?: 0L)
lastChapterId = null
}
}
override fun onUpdateEmptyView(size: Int) {
if (size > 0) {
empty_view?.hide()
} else {
empty_view?.show(R.drawable.ic_history_white_128dp, R.string
.information_no_recent_manga)
}
}
fun updateChapterDownload(download: Download) {
if (view == null) return
for (i in 0 until adapter.itemCount) {
val holder = recycler.findViewHolderForAdapterPosition(i) as? RecentsHolder ?: continue
if (holder.updateChapterDownload(download)) break
}
}
fun refreshItem(chapterId: Long) {
for (i in 0 until adapter.itemCount) {
val holder = recycler.findViewHolderForAdapterPosition(i) as? RecentsHolder ?: continue
holder.refreshChapter(chapterId)
}
}
override fun downloadChapter(item: RecentMangaItem) {
val view = view ?: return
val chapter = item.chapter
val manga = item.mch.manga
if (item.status != Download.NOT_DOWNLOADED && item.status != Download.ERROR) {
presenter.deleteChapter(chapter, manga)
} else {
if (item.status == Download.ERROR) DownloadService.start(view.context)
else presenter.downloadChapter(manga, chapter)
}
}
override fun downloadChapterNow(chapter: Chapter) {
presenter.startDownloadChapterNow(chapter)
}
override fun showManga(manga: Manga) = router.pushController(MangaDetailsController(manga).withFadeTransaction())
override fun resumeManga(manga: Manga, chapter: Chapter) {
val activity = activity ?: return
val intent = ReaderActivity.newIntent(activity, manga, chapter)
startActivity(intent)
}
override fun markAsRead(manga: Manga, chapter: Chapter) {
val lastRead = chapter.last_page_read
val pagesLeft = chapter.pages_left
lastChapterId = chapter.id
presenter.markChapterRead(chapter, true)
snack =
view?.snack(R.string.marked_as_read, Snackbar.LENGTH_INDEFINITE) {
anchorView = activity?.bottom_nav
var undoing = false
setAction(R.string.action_undo) {
presenter.markChapterRead(chapter, false, lastRead, pagesLeft)
undoing = true
}
addCallback(object : BaseTransientBottomBar.BaseCallback<Snackbar>() {
override fun onDismissed(transientBottomBar: Snackbar?, event: Int) {
super.onDismissed(transientBottomBar, event)
if (!undoing && presenter.preferences.removeAfterMarkedAsRead()) {
lastChapterId = chapter.id
presenter.deleteChapter(chapter, manga)
}
}
})
}
(activity as? MainActivity)?.setUndoSnackBar(snack)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.recents, menu)
val searchItem = menu.findItem(R.id.action_search)
val searchView = searchItem.actionView as SearchView
searchView.queryHint = view?.context?.getString(R.string.search_recents)
if (presenter.query.isNotEmpty()) {
searchItem.expandActionView()
searchView.setQuery(presenter.query, true)
searchView.clearFocus()
}
setOnQueryTextChangeListener(searchView) {
if (presenter.query != it) {
presenter.query = it ?: return@setOnQueryTextChangeListener false
refresh()
}
true
}
}
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
super.onChangeStarted(handler, type)
if (type.isEnter) {
if (type == ControllerChangeType.POP_EXIT) {
presenter.onCreate()
}
setHasOptionsMenu(true)
} else {
snack?.dismiss()
setHasOptionsMenu(false)
}
}
override fun setCover(manga: Manga, view: ImageView) {
val activity = activity ?: return
GlideApp.with(activity).load(manga).diskCacheStrategy(DiskCacheStrategy.AUTOMATIC)
.signature(ObjectKey(MangaImpl.getLastCoverFetch(manga.id!!).toString())).into(view)
}
override fun viewAll(position: Int) {
val recentsType = (adapter.getItem(position) as? RecentsItem)?.recentType ?: return
val controller: Controller = when (recentsType) {
RecentsItem.NEW_CHAPTERS -> RecentChaptersController()
RecentsItem.CONTINUE_READING -> RecentlyReadController()
else -> return
}
router.pushController(controller.withFadeTransaction())
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_refresh -> {
if (!LibraryUpdateService.isRunning()) {
val view = view ?: return true
LibraryUpdateService.start(view.context)
snack = view.snack(R.string.updating_library) {
anchorView = (activity as? MainActivity)?.bottom_nav
}
}
}
}
return super.onOptionsItemSelected(item)
}
}

View File

@ -0,0 +1,132 @@
package eu.kanade.tachiyomi.ui.recents
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import eu.kanade.tachiyomi.util.system.dpToPx
import eu.kanade.tachiyomi.util.view.updateLayoutParams
import eu.kanade.tachiyomi.util.view.visibleIf
import kotlinx.android.synthetic.main.recents_item.*
class RecentsHolder(
view: View,
val adapter: RecentsAdapter
) : BaseFlexibleViewHolder(view, adapter),
FlexibleAdapter.OnItemClickListener,
FlexibleAdapter.OnUpdateListener,
RecentMangaAdapter.RecentsInterface {
private val subAdapter = RecentMangaAdapter(this)
init {
recycler.adapter = subAdapter
subAdapter.isSwipeEnabled = true
val manager = LinearLayoutManager(view.context)
recycler.layoutManager = manager
recycler.addItemDecoration(
DividerItemDecoration(
recycler.context, DividerItemDecoration.VERTICAL
)
)
recycler.setHasFixedSize(true)
view_all.setOnClickListener { adapter.delegate.viewAll(adapterPosition) }
subAdapter.itemTouchHelperCallback.setSwipeFlags(
ItemTouchHelper.LEFT
)
}
fun bind(item: RecentsItem) {
when (item.recentType) {
RecentsItem.CONTINUE_READING -> {
title.setText(R.string.continue_reading)
view_all.setText(R.string.view_history)
}
RecentsItem.NEW_CHAPTERS -> {
title.setText(R.string.new_chapters)
view_all.setText(R.string.view_all_updates)
}
RecentsItem.NEWLY_ADDED -> {
title.setText(R.string.new_additions)
}
}
title.visibleIf(item.recentType != RecentsItem.SEARCH)
view_all.visibleIf(item.recentType == RecentsItem.CONTINUE_READING || item.recentType == RecentsItem.NEW_CHAPTERS)
recycler.updateLayoutParams<ViewGroup.MarginLayoutParams> {
bottomMargin = if (adapterPosition == adapter.itemCount - 1) 0 else 12.dpToPx
}
subAdapter.updateDataSet(item.mangaList)
}
override fun downloadChapter(position: Int) {
val item = (subAdapter.getItem(position) as? RecentMangaItem) ?: return
adapter.delegate.downloadChapter(item)
}
override fun startDownloadNow(position: Int) {
val chapter = (subAdapter.getItem(position) as? RecentMangaItem)?.chapter ?: return
adapter.delegate.downloadChapterNow(chapter)
}
override fun onCoverClick(position: Int) {
val manga = (subAdapter.getItem(position) as? RecentMangaItem)?.mch?.manga ?: return
adapter.delegate.showManga(manga)
}
override fun onItemClick(view: View?, position: Int): Boolean {
val item = (subAdapter.getItem(position) as? RecentMangaItem) ?: return false
adapter.delegate.resumeManga(item.mch.manga, item.chapter)
return true
}
fun updateChapterDownload(download: Download): Boolean {
val holder = getHolder(download.chapter) ?: return false
holder.notifyStatus(download.status, download.progress)
return true
}
fun refreshChapter(chapterId: Long) {
val item = (adapter.getItem(adapterPosition) as? RecentsItem) ?: return
val recentItemPos = item.mangaList.indexOfFirst { it.mch.chapter.id == chapterId }
if (recentItemPos > -1) subAdapter.notifyItemChanged(recentItemPos)
}
override fun onUpdateEmptyView(size: Int) {
if (size > 0) {
empty_view?.hide()
} else {
val recentsType = (adapter.getItem(adapterPosition) as? RecentsItem)?.recentType ?: return
when (recentsType) {
RecentsItem.CONTINUE_READING ->
empty_view?.show(R.drawable.ic_history_white_128dp, R.string.information_no_recent_manga)
RecentsItem.NEW_CHAPTERS ->
empty_view?.show(R.drawable.ic_update_black_128dp, R.string.information_no_recent)
RecentsItem.NEWLY_ADDED ->
empty_view?.show(R.drawable.recent_read_outline_128dp, R.string.information_no_recent)
RecentsItem.SEARCH ->
empty_view?.show(R.drawable.search_128dp, R.string.no_search_result)
}
}
}
private fun getHolder(chapter: Chapter): RecentMangaHolder? {
return recycler?.findViewHolderForItemId(chapter.id!!) as? RecentMangaHolder
}
override fun setCover(manga: Manga, view: ImageView) {
adapter.delegate.setCover(manga, view)
}
override fun markAsRead(position: Int) {
val item = (subAdapter.getItem(position) as RecentMangaItem)
adapter.delegate.markAsRead(item.mch.manga, item.chapter)
}
}

View File

@ -0,0 +1,51 @@
package eu.kanade.tachiyomi.ui.recents
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
class RecentsItem(val recentType: Int, val mangaList: List<RecentMangaItem>) :
AbstractFlexibleItem<RecentsHolder>() {
override fun getLayoutRes(): Int {
return R.layout.recents_item
}
override fun createViewHolder(
view: View,
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>
): RecentsHolder {
return RecentsHolder(view, adapter as RecentsAdapter)
}
override fun bindViewHolder(
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
holder: RecentsHolder,
position: Int,
payloads: MutableList<Any?>?
) {
holder.bind(this)
}
override fun equals(other: Any?): Boolean {
if (other is RecentsItem) {
return recentType == other.recentType
}
return false
}
override fun hashCode(): Int {
return recentType.hashCode()
}
companion object {
const val CONTINUE_READING = 0
const val NEW_CHAPTERS = 1
const val NEWLY_ADDED = 2
const val SEARCH = 3
}
}

View File

@ -0,0 +1,189 @@
package eu.kanade.tachiyomi.ui.recents
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.LibraryManga
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
import eu.kanade.tachiyomi.data.library.LibraryServiceListener
import eu.kanade.tachiyomi.data.library.LibraryUpdateService
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.util.system.executeOnIO
import java.util.Calendar
import java.util.Date
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class RecentsPresenter(
val controller: RecentsController,
val preferences: PreferencesHelper = Injekt.get(),
private val downloadManager: DownloadManager = Injekt.get(),
private val db: DatabaseHelper = Injekt.get()
) : DownloadQueue.DownloadListener, LibraryServiceListener {
private var scope = CoroutineScope(Job() + Dispatchers.Default)
private var recentItems = listOf<RecentMangaItem>()
var groupedRecentItems = listOf<RecentsItem>()
var query = ""
fun onCreate() {
downloadManager.addListener(this)
LibraryUpdateService.setListener(this)
getRecents()
}
fun getRecents() {
scope.launch {
val cal = Calendar.getInstance()
cal.time = Date()
if (query.isNotEmpty()) cal.add(Calendar.YEAR, -50)
else cal.add(Calendar.MONTH, -1)
val cReading =
if (query.isEmpty())
db.getRecentsWithUnread(cal.time, query).executeOnIO()
else
db.getRecentMangaLimit(cal.time, 8, query).executeOnIO()
val rUpdates = db.getUpdatedManga(cal.time, query).executeOnIO()
rUpdates.forEach {
it.history.last_read = it.chapter.date_upload
}
val mangaList = (cReading + rUpdates).sortedByDescending {
it.history.last_read
}.distinctBy {
if (query.isEmpty()) it.manga.id else it.chapter.id }
recentItems = mangaList.mapNotNull {
val chapter = if (it.chapter.read) getNextChapter(it.manga)
else it.chapter
if (chapter == null) if (query.isNotEmpty()) RecentMangaItem(it, it.chapter)
else null
else RecentMangaItem(it, chapter)
}
setDownloadedChapters(recentItems)
if (query.isEmpty()) {
val nChaptersItems = RecentsItem(
RecentsItem.NEW_CHAPTERS,
recentItems.filter { it.mch.history.id == null }.take(4)
)
val cReadingItems = RecentsItem(
RecentsItem.CONTINUE_READING,
recentItems.filter { it.mch.history.id != null }.take(
8 - nChaptersItems.mangaList.size
)
)
// TODO: Add Date Added
groupedRecentItems = listOf(cReadingItems, nChaptersItems).sortedByDescending {
it.mangaList.firstOrNull()?.mch?.history?.last_read ?: 0
}
} else {
groupedRecentItems = listOf(RecentsItem(RecentsItem.SEARCH, recentItems))
}
withContext(Dispatchers.Main) { controller.showLists(groupedRecentItems) }
}
}
private fun getNextChapter(manga: Manga): Chapter? {
val chapters = db.getChapters(manga).executeAsBlocking()
return chapters.sortedByDescending { it.source_order }.find { !it.read }
}
fun onDestroy() {
downloadManager.removeListener(this)
LibraryUpdateService.removeListener(this)
}
fun cancelScope() {
scope.cancel()
}
/**
* Finds and assigns the list of downloaded chapters.
*
* @param chapters the list of chapter from the database.
*/
private fun setDownloadedChapters(chapters: List<RecentMangaItem>) {
for (item in chapters) {
if (downloadManager.isChapterDownloaded(item.chapter, item.mch.manga)) {
item.status = Download.DOWNLOADED
} else if (downloadManager.hasQueue()) {
item.status = downloadManager.queue.find { it.chapter.id == item.chapter.id }
?.status ?: 0
}
}
}
override fun updateDownload(download: Download) {
recentItems.find { it.chapter.id == download.chapter.id }?.download = download
scope.launch(Dispatchers.Main) {
controller.updateChapterDownload(download)
}
}
override fun onUpdateManga(manga: LibraryManga) {
getRecents()
}
/**
* Deletes the given list of chapter.
* @param chapter the chapter to delete.
*/
fun deleteChapter(chapter: Chapter, manga: Manga, update: Boolean = true) {
val source = Injekt.get<SourceManager>().getOrStub(manga.source)
downloadManager.deleteChapters(listOf(chapter), manga, source)
if (update) {
val item = recentItems.find { it.chapter.id == chapter.id } ?: return
item.apply {
status = Download.NOT_DOWNLOADED
download = null
}
controller.showLists(groupedRecentItems)
}
}
/**
* Downloads the given list of chapters with the manager.
* @param chapter the chapter to download.
*/
fun downloadChapter(manga: Manga, chapter: Chapter) {
downloadManager.downloadChapters(manga, listOf(chapter))
}
fun startDownloadChapterNow(chapter: Chapter) {
downloadManager.startDownloadNow(chapter)
}
/**
* Mark the selected chapter list as read/unread.
* @param selectedChapters the list of selected chapters.
* @param read whether to mark chapters as read or unread.
*/
fun markChapterRead(
chapter: Chapter,
read: Boolean,
lastRead: Int? = null,
pagesLeft: Int? = null
) {
scope.launch(Dispatchers.IO) {
chapter.apply {
this.read = read
if (!read) {
last_page_read = lastRead ?: 0
pages_left = pagesLeft ?: 0
}
}
db.updateChaptersProgress(listOf(chapter)).executeAsBlocking()
getRecents()
}
}
}

View File

@ -0,0 +1,19 @@
package eu.kanade.tachiyomi.util.system
import com.pushtorefresh.storio.sqlite.operations.get.PreparedGetListOfObjects
import com.pushtorefresh.storio.sqlite.operations.get.PreparedGetObject
import com.pushtorefresh.storio.sqlite.operations.put.PreparedPutObject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
suspend fun <T> PreparedGetListOfObjects<T>.executeOnIO(): List<T> {
return withContext(Dispatchers.IO) { executeAsBlocking() }
}
suspend fun <T> PreparedGetObject<T>.executeOnIO(): T? {
return withContext(Dispatchers.IO) { executeAsBlocking() }
}
suspend fun <T> PreparedPutObject<T>.executeOnIO() {
withContext(Dispatchers.IO) { executeAsBlocking() }
}

View File

@ -323,6 +323,7 @@ fun Controller.scrollViewWith(
recycler: RecyclerView, recycler: RecyclerView,
padBottom: Boolean = false, padBottom: Boolean = false,
customPadding: Boolean = false, customPadding: Boolean = false,
skipFirstSnap: Boolean = false,
swipeRefreshLayout: SwipeRefreshLayout? = null, swipeRefreshLayout: SwipeRefreshLayout? = null,
afterInsets: ((WindowInsets) -> Unit)? = null afterInsets: ((WindowInsets) -> Unit)? = null
) { ) {
@ -370,7 +371,7 @@ fun Controller.scrollViewWith(
val closerToTop = abs(activity!!.appbar.y) - halfWay > 0 val closerToTop = abs(activity!!.appbar.y) - halfWay > 0
val atTop = (!customPadding && val atTop = (!customPadding &&
(recycler.layoutManager as LinearLayoutManager) (recycler.layoutManager as LinearLayoutManager)
.findFirstVisibleItemPosition() < 2) || .findFirstVisibleItemPosition() < 2 && !skipFirstSnap) ||
!recycler.canScrollVertically(-1) !recycler.canScrollVertically(-1)
activity!!.appbar.animate().y( activity!!.appbar.animate().y(
if (closerToTop && !atTop) (-activity!!.appbar.height.toFloat()) if (closerToTop && !atTop) (-activity!!.appbar.height.toFloat())

View File

@ -0,0 +1,8 @@
<!-- drawable/clock_outline.xml -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="128dp"
android:width="128dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#000" android:pathData="M12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22C6.47,22 2,17.5 2,12A10,10 0 0,1 12,2M12.5,7V12.25L17,14.92L16.25,16.15L11,13V7H12.5Z" />
</vector>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
tools:background="#FFFF00"
android:color="?android:attr/colorAccent">
<item android:id="@android:id/mask">
<shape android:shape="rectangle">
<solid android:color="?android:attr/colorAccent" />
<corners android:radius="80dp" />
</shape>
</item>
<item android:id="@android:id/background">
<shape
android:shape="rectangle">
<corners android:radius="80dp"/>
<size
android:height="100dp"
android:width="100dp" />
<solid android:color="#AD212121"/>
<stroke android:width="0.1dp" android:color="#EDEDED" />
</shape>
</item>
</ripple>

View File

@ -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="M15.5,14h-0.79l-0.28,-0.27C15.41,12.59 16,11.11 16,9.5 16,5.91 13.09,3 9.5,3S3,5.91 3,9.5 5.91,16 9.5,16c1.61,0 3.09,-0.59 4.23,-1.57l0.27,0.28v0.79l5,4.99L20.49,19l-4.99,-5zM9.5,14C7.01,14 5,11.99 5,9.5S7.01,5 9.5,5 14,7.01 14,9.5 11.99,14 9.5,14z"/>
</vector>

View File

@ -1,11 +1,11 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"> android:orientation="vertical">
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipe_refresh" android:id="@+id/swipe_refresh"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
@ -18,18 +18,18 @@
android:id="@+id/recycler" android:id="@+id/recycler"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:descendantFocusability="blocksDescendants"
android:clipToPadding="false" android:clipToPadding="false"
tools:listitem="@layout/recent_chapters_item"/> android:descendantFocusability="blocksDescendants"
tools:listitem="@layout/recent_chapters_item" />
<eu.kanade.tachiyomi.widget.EmptyView <eu.kanade.tachiyomi.widget.EmptyView
android:id="@+id/empty_view" android:id="@+id/empty_view"
android:visibility="gone"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
android:layout_height="wrap_content"/> android:visibility="gone" />
</FrameLayout> </FrameLayout>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout> </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,177 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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"
android:id="@+id/recent_manga_layout"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:background="?android:attr/colorBackground">
<FrameLayout
android:id="@+id/right_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
android:background="@color/material_green_800"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="@+id/read"
android:layout_width="24dp"
android:layout_height="24dp"
android:tint="@color/md_white_1000"
android:layout_gravity="end|center"
android:layout_marginEnd="21dp"
android:src="@drawable/eye" />
</FrameLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/front_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:background="?attr/selectable_list_drawable"
android:minHeight="@dimen/material_component_lists_single_line_with_avatar_height">
<FrameLayout
android:id="@+id/card_layout"
android:layout_width="60dp"
android:layout_height="80dp"
android:layout_marginStart="12dp"
android:layout_marginTop="6dp"
android:layout_marginBottom="6dp"
app:layout_constrainedHeight="true"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.0">
<com.google.android.material.card.MaterialCardView
android:id="@+id/card"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center">
<ImageView
android:id="@+id/cover_thumbnail"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:adjustViewBounds="true"
android:background="?android:attr/colorBackground"
android:maxHeight="60dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:background="?android:attr/colorBackground"
tools:ignore="ContentDescription"
tools:src="@mipmap/ic_launcher" />
</com.google.android.material.card.MaterialCardView>
</FrameLayout>
<com.google.android.material.textview.MaterialTextView
android:id="@+id/title"
style="@style/TextAppearance.Regular.Body1.SemiBold"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="2dp"
android:layout_marginEnd="16dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?android:attr/textColorPrimary"
android:textSize="16sp"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@+id/card_layout"
app:layout_constraintTop_toTopOf="parent"
tools:text="New Chapter" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/subtitle"
style="@style/TextAppearance.MaterialComponents.Body1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginBottom="14dp"
android:ellipsize="end"
android:maxLines="1"
android:singleLine="true"
android:textColor="?android:attr/textColorPrimary"
android:textSize="14sp"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="@+id/title"
app:layout_constraintTop_toBottomOf="@+id/title"
app:layout_constraintVertical_bias="0.0"
tools:text="Manga title" />
<com.google.android.material.textview.MaterialTextView
android:id="@+id/body"
style="@style/TextAppearance.MaterialComponents.Body1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="6dp"
android:layout_marginBottom="14dp"
android:ellipsize="end"
android:maxLines="2"
android:singleLine="true"
android:textColor="?android:attr/textColorSecondary"
android:textSize="14sp"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toStartOf="@+id/download_button"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="@+id/title"
app:layout_constraintTop_toBottomOf="@+id/subtitle"
app:layout_constraintVertical_bias="0.0"
tools:text="3 Days ago" />
<ProgressBar
android:id="@+id/progress"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="0dp"
android:layout_height="3dp"
android:layout_gravity="bottom"
android:layout_marginTop="4dp"
android:layout_marginEnd="6dp"
android:background="#66000000"
android:indeterminate="false"
android:progress="10"
android:progressTint="?colorAccent"
android:visibility="gone"
app:layout_constraintEnd_toStartOf="@id/download_button"
app:layout_constraintStart_toStartOf="@id/title"
app:layout_constraintTop_toBottomOf="@id/subtitle" />
<include
layout="@layout/download_button"
android:layout_width="50dp"
android:layout_height="0dp"
android:layout_marginEnd="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/subtitle" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/bottom_line"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="6dp"
android:orientation="horizontal"
app:barrierDirection="bottom"
app:constraint_referenced_ids="card_layout,body" />
<View
android:id="@+id/padding"
android:layout_width="0dp"
android:layout_height="6dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@+id/bottom_line" />
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>

View File

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
android:id="@+id/recent_item"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="4dp"
android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="@string/label_recent_updates" />
<com.google.android.material.button.MaterialButton
android:id="@+id/view_all"
style="@style/Theme.Widget.Button.TextButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="12dp"
android:text="@string/view_all"
app:layout_constraintBaseline_toBaselineOf="@id/title"
app:layout_constraintEnd_toEndOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_marginBottom="12dp"
android:nestedScrollingEnabled="true"
android:clipToPadding="false"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/title" />
<eu.kanade.tachiyomi.widget.EmptyView
android:id="@+id/empty_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/title"
android:visibility="gone"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -8,7 +8,7 @@
android:title="@string/label_library" /> android:title="@string/label_library" />
<item <item
android:id="@+id/nav_recents" android:id="@+id/nav_recents"
android:icon="@drawable/recent_updates_selector_24dp" android:icon="@drawable/recent_read_selector_24dp"
android:title="@string/short_recents" /> android:title="@string/short_recents" />
<item <item
android:id="@+id/nav_catalogues" android:id="@+id/nav_catalogues"

View File

@ -1,5 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"> <menu xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@+id/action_start"
android:title="@string/start_downloading_now"
android:icon="@drawable/ic_delete_white_24dp"/>
<item android:id="@+id/action_delete" <item android:id="@+id/action_delete"
android:title="@string/action_remove_download" android:title="@string/action_remove_download"
android:icon="@drawable/ic_delete_white_24dp"/> android:icon="@drawable/ic_delete_white_24dp"/>

View File

@ -1,25 +1,9 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<item <item
android:id="@+id/action_search" android:id="@+id/action_sort"
android:icon="@drawable/ic_search_white_24dp" android:icon="@drawable/ic_sort_white_24dp"
android:title="@string/action_search" android:title="@string/action_sort"
android:visible="false" app:showAsAction="ifRoom" />
app:actionViewClass="androidx.appcompat.widget.SearchView"
app:showAsAction="ifRoom|collapseActionView" />
<item
android:id="@+id/action_recents"
android:icon="@drawable/ic_history_black_24dp"
android:title="@string/label_recent_manga"
app:showAsAction="never" />
<item
android:id="@+id/action_settings"
android:icon="@drawable/ic_settings_white_24dp"
android:title="@string/label_settings"
app:showAsAction="never"
/>
</menu> </menu>

View File

@ -4,20 +4,6 @@
android:id="@+id/action_search" android:id="@+id/action_search"
android:icon="@drawable/ic_search_white_24dp" android:icon="@drawable/ic_search_white_24dp"
android:title="@string/action_search" android:title="@string/action_search"
android:visible="false"
app:actionViewClass="androidx.appcompat.widget.SearchView" app:actionViewClass="androidx.appcompat.widget.SearchView"
app:showAsAction="ifRoom|collapseActionView" /> app:showAsAction="ifRoom|collapseActionView" />
<item
android:id="@+id/action_recents"
android:icon="@drawable/ic_update_black_24dp"
android:title="@string/label_recent_updates"
app:showAsAction="never" />
<item
android:id="@+id/action_settings"
android:icon="@drawable/ic_settings_white_24dp"
android:title="@string/label_settings"
app:showAsAction="never"
/>
</menu> </menu>

View File

@ -0,0 +1,23 @@
<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">
<item
android:id="@+id/action_search"
android:icon="@drawable/ic_search_white_24dp"
android:title="@string/action_search"
app:actionViewClass="androidx.appcompat.widget.SearchView"
android:visible="false"
app:showAsAction="ifRoom|collapseActionView" />
<item
android:id="@+id/action_refresh"
android:icon="@drawable/ic_refresh_white_24dp"
android:title="@string/action_update_library"
app:showAsAction="never" />
<item
android:id="@+id/action_settings"
android:icon="@drawable/ic_settings_white_24dp"
android:title="@string/label_settings"
app:showAsAction="never" />
</menu>

View File

@ -19,7 +19,9 @@
<string name="label_library">Library</string> <string name="label_library">Library</string>
<string name="label_recent_manga">Recently read</string> <string name="label_recent_manga">Recently read</string>
<string name="label_catalogues">Catalogues</string> <string name="label_catalogues">Catalogues</string>
<string name="label_recent_updates">Recently updated</string> <string name="label_recent_updates">Recent updates</string>
<string name="new_chapters">New chapters</string>
<string name="new_additions">New additions</string>
<string name="label_selected">Selected: %1$d</string> <string name="label_selected">Selected: %1$d</string>
<string name="label_migration">Source migration</string> <string name="label_migration">Source migration</string>
<string name="label_extensions">Extensions</string> <string name="label_extensions">Extensions</string>
@ -84,6 +86,7 @@
<string name="action_remove_bookmark">Remove bookmark</string> <string name="action_remove_bookmark">Remove bookmark</string>
<string name="action_delete">Delete</string> <string name="action_delete">Delete</string>
<string name="action_remove_download">Remove download</string> <string name="action_remove_download">Remove download</string>
<string name="start_downloading_now">Start downloading now</string>
<string name="action_update_library">Update library</string> <string name="action_update_library">Update library</string>
<string name="action_edit">Edit</string> <string name="action_edit">Edit</string>
<string name="action_edit_cover">Edit cover</string> <string name="action_edit_cover">Edit cover</string>
@ -459,7 +462,6 @@
<item quantity="one">New chapter</item> <item quantity="one">New chapter</item>
<item quantity="other">%d unread</item> <item quantity="other">%d unread</item>
</plurals> </plurals>
<string name="new_chapter">New</string>
<plurals name="download_count"> <plurals name="download_count">
<item quantity="one">1 downloaded</item> <item quantity="one">1 downloaded</item>
<item quantity="other">%d downloaded</item> <item quantity="other">%d downloaded</item>
@ -754,4 +756,14 @@
<string name="sort_by_chapter_number">Sort by chapter number</string> <string name="sort_by_chapter_number">Sort by chapter number</string>
<string name="newest_first">Newest to oldest</string> <string name="newest_first">Newest to oldest</string>
<string name="oldest_first">Oldest to newest</string> <string name="oldest_first">Oldest to newest</string>
<string name="view_all">View All</string>
<string name="previously_read_chapter">Previously read Chapter %1$s</string>
<string name="last_read_chapter_x">Last read Chapter %1$s</string>
<string name="last_read_x">Last read %1$s</string>
<string name="uploaded_x">Uploaded %1$s</string>
<string name="view_history">View history</string>
<string name="view_all_updates">View all updates</string>
<string name="marked_as_read">Marked as read</string>
<string name="no_search_result">No search results</string>
<string name="search_recents">Search recents…</string>
</resources> </resources>