Download manager rewrite (#535)

* Saving to SD working

* Rename imagePath to uri

* Handle android < 21

* Minor changes

* Separate downloader from the manager. Optimize folder lookups

* Persist downloads across restarts

* Fix for #511

* Updated ReactiveNetwork. Add some documentation

* More documentation and minor fixes

* Handle persistent notifications. Other minor changes

* Improve downloader and add documentation

* Rename pageNumber to index in Page class

* Remove unused methods

* Use chop method

* Make sure dest dir is created

* Reset downloads dir preference

* Use invalidate options menu in download fragment and fix wrong condition

* Fix empty download queue after application restart

* Use addAll method in download queue to avoid too many notifications

* Inform download manager changes
This commit is contained in:
inorichi
2016-11-20 11:20:57 +01:00
committed by GitHub
parent 59c626b4a8
commit 6f297161de
34 changed files with 1325 additions and 855 deletions

View File

@@ -6,6 +6,7 @@ import android.view.*
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.DownloadService
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.util.plusAssign
@@ -30,21 +31,6 @@ class DownloadFragment : BaseRxFragment<DownloadPresenter>() {
*/
private lateinit var adapter: DownloadAdapter
/**
* Menu item to start the queue.
*/
private var startButton: MenuItem? = null
/**
* Menu item to pause the queue.
*/
private var pauseButton: MenuItem? = null
/**
* Menu item to clear the queue.
*/
private var clearButton: MenuItem? = null
/**
* Subscription list to be cleared during [onDestroyView].
*/
@@ -95,15 +81,15 @@ class DownloadFragment : BaseRxFragment<DownloadPresenter>() {
recycler.setHasFixedSize(true)
// Suscribe to changes
subscriptions += presenter.downloadManager.runningSubject
subscriptions += DownloadService.runningRelay
.observeOn(AndroidSchedulers.mainThread())
.subscribe { onQueueStatusChange(it) }
subscriptions += presenter.getStatusObservable()
subscriptions += presenter.getDownloadStatusObservable()
.observeOn(AndroidSchedulers.mainThread())
.subscribe { onStatusChange(it) }
subscriptions += presenter.getProgressObservable()
subscriptions += presenter.getDownloadProgressObservable()
.observeOn(AndroidSchedulers.mainThread())
.subscribe { onUpdateDownloadedPages(it) }
}
@@ -119,23 +105,17 @@ class DownloadFragment : BaseRxFragment<DownloadPresenter>() {
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.download_queue, menu)
}
override fun onPrepareOptionsMenu(menu: Menu) {
// Set start button visibility.
startButton = menu.findItem(R.id.start_queue).apply {
isVisible = !isRunning && !presenter.downloadQueue.isEmpty()
}
menu.findItem(R.id.start_queue).isVisible = !isRunning && !presenter.downloadQueue.isEmpty()
// Set pause button visibility.
pauseButton = menu.findItem(R.id.pause_queue).apply {
isVisible = isRunning
}
menu.findItem(R.id.pause_queue).isVisible = isRunning
// Set clear button visibility.
clearButton = menu.findItem(R.id.clear_queue).apply {
if (!presenter.downloadQueue.isEmpty()) {
isVisible = true
}
}
menu.findItem(R.id.clear_queue).isVisible = !presenter.downloadQueue.isEmpty()
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
@@ -182,7 +162,7 @@ class DownloadFragment : BaseRxFragment<DownloadPresenter>() {
// Get the sum of percentages for all the pages.
.flatMap {
Observable.from(download.pages)
.map { it.progress }
.map(Page::progress)
.reduce { x, y -> x + y }
}
// Keep only the latest emission to avoid backpressure.
@@ -218,9 +198,7 @@ class DownloadFragment : BaseRxFragment<DownloadPresenter>() {
*/
private fun onQueueStatusChange(running: Boolean) {
isRunning = running
startButton?.isVisible = !running && !presenter.downloadQueue.isEmpty()
pauseButton?.isVisible = running
clearButton?.isVisible = !presenter.downloadQueue.isEmpty()
activity.supportInvalidateOptionsMenu()
// Check if download queue is empty and update information accordingly.
setInformationView()
@@ -232,13 +210,11 @@ class DownloadFragment : BaseRxFragment<DownloadPresenter>() {
* @param downloads the downloads from the queue.
*/
fun onNextDownloads(downloads: List<Download>) {
activity.supportInvalidateOptionsMenu()
setInformationView()
adapter.setItems(downloads)
}
fun onDownloadRemoved(position: Int) {
adapter.notifyItemRemoved(position)
}
/**
* Called when the progress of a download changes.
*

View File

@@ -29,36 +29,21 @@ class DownloadPresenter : BasePresenter<DownloadFragment>() {
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
Observable.just(ArrayList(downloadQueue))
.doOnNext { syncQueue(it) }
.subscribeLatestCache({ view, downloads ->
view.onNextDownloads(downloads)
}, { view, error ->
downloadQueue.getUpdatedObservable()
.observeOn(AndroidSchedulers.mainThread())
.map { ArrayList(it) }
.subscribeLatestCache(DownloadFragment::onNextDownloads, { view, error ->
Timber.e(error)
})
}
private fun syncQueue(queue: MutableList<Download>) {
add(downloadQueue.getRemovedObservable()
.observeOn(AndroidSchedulers.mainThread())
.subscribe { download ->
val position = queue.indexOf(download)
if (position != -1) {
queue.removeAt(position)
@Suppress("DEPRECATION")
view?.onDownloadRemoved(position)
}
})
}
fun getStatusObservable(): Observable<Download> {
fun getDownloadStatusObservable(): Observable<Download> {
return downloadQueue.getStatusObservable()
.startWith(downloadQueue.getActiveDownloads())
}
fun getProgressObservable(): Observable<Download> {
fun getDownloadProgressObservable(): Observable<Download> {
return downloadQueue.getProgressObservable()
.onBackpressureBuffer()
}

View File

@@ -185,15 +185,10 @@ class LibraryPresenter : BasePresenter<LibraryFragment>() {
}
if (prefFilterDownloaded) {
val mangaDir = downloadManager.getAbsoluteMangaDirectory(source, manga)
val mangaDir = downloadManager.findMangaDir(source, manga)
if (mangaDir.exists()) {
for (file in mangaDir.listFiles()) {
if (file.isDirectory && file.listFiles().isNotEmpty()) {
hasDownloaded = true
break
}
}
if (mangaDir != null) {
hasDownloaded = mangaDir.listFiles()?.any { it.isDirectory } ?: false
}
}

View File

@@ -38,7 +38,7 @@ class ChangelogDialogFragment : DialogFragment() {
override fun onCreateDialog(savedState: Bundle?): Dialog {
val view = WhatsNewRecyclerView(context)
return MaterialDialog.Builder(activity)
.title("Changelog")
.title(if (BuildConfig.DEBUG) "Notices" else "Changelog")
.customView(view, false)
.positiveText(android.R.string.yes)
.build()

View File

@@ -132,6 +132,9 @@ class ChaptersPresenter : BasePresenter<ChaptersFragment>() {
chapters.map { it.toModel() }
}
.doOnNext { chapters ->
// Find downloaded chapters
setDownloadedChapters(chapters)
// Store the last emission
this.chapters = chapters
@@ -157,16 +160,25 @@ class ChaptersPresenter : BasePresenter<ChaptersFragment>() {
if (download != null) {
// If there's an active download, assign it.
model.download = download
} else {
// Otherwise ask the manager if the chapter is downloaded and assign it to the status.
model.status = if (downloadManager.isChapterDownloaded(source, manga, this))
Download.DOWNLOADED
else
Download.NOT_DOWNLOADED
}
return model
}
/**
* Finds and assigns the list of downloaded chapters.
*
* @param chapters the list of chapter from the database.
*/
private fun setDownloadedChapters(chapters: List<ChapterModel>) {
val files = downloadManager.findMangaDir(source, manga)?.listFiles() ?: return
val cached = mutableMapOf<Chapter, String>()
files.mapNotNull { it.name }
.mapNotNull { name -> chapters.find {
name == cached.getOrPut(it) { downloadManager.getChapterDirName(it) }
} }
.forEach { it.status = Download.DOWNLOADED }
}
/**
* Requests an updated list of chapters from the source.
*/
@@ -318,10 +330,6 @@ class ChaptersPresenter : BasePresenter<ChaptersFragment>() {
* @param chapters the list of chapters to delete.
*/
fun deleteChapters(chapters: List<ChapterModel>) {
val wasRunning = downloadManager.isRunning
if (wasRunning) {
DownloadService.stop(context)
}
Observable.from(chapters)
.doOnNext { deleteChapter(it) }
.toList()
@@ -330,9 +338,6 @@ class ChaptersPresenter : BasePresenter<ChaptersFragment>() {
.observeOn(AndroidSchedulers.mainThread())
.subscribeFirst({ view, result ->
view.onChaptersDeleted()
if (wasRunning) {
DownloadService.start(context)
}
}, { view, error ->
view.onChaptersDeletedError(error)
})
@@ -343,7 +348,7 @@ class ChaptersPresenter : BasePresenter<ChaptersFragment>() {
* @param chapter the chapter to delete.
*/
private fun deleteChapter(chapter: ChapterModel) {
downloadManager.queue.del(chapter)
downloadManager.queue.remove(chapter)
downloadManager.deleteChapter(source, manga, chapter)
chapter.status = Download.NOT_DOWNLOADED
chapter.download = null

View File

@@ -70,14 +70,15 @@ class ChapterLoader(
private fun retrievePageList(chapter: ReaderChapter) = Observable.just(chapter)
.flatMap {
// Check if the chapter is downloaded.
chapter.isDownloaded = downloadManager.isChapterDownloaded(source, manga, chapter)
chapter.isDownloaded = downloadManager.findChapterDir(source, manga, chapter) != null
// Fetch the page list from disk.
if (chapter.isDownloaded)
Observable.just(downloadManager.getSavedPageList(source, manga, chapter)!!)
// Fetch the page list from cache or fallback to network
else
if (chapter.isDownloaded) {
// Fetch the page list from disk.
downloadManager.buildPageList(source, manga, chapter)
} else {
// Fetch the page list from cache or fallback to network
source.fetchPageList(chapter)
}
}
.doOnNext { pages ->
chapter.pages = pages
@@ -85,21 +86,11 @@ class ChapterLoader(
}
private fun loadPages(chapter: ReaderChapter) {
if (chapter.isDownloaded) {
loadDownloadedPages(chapter)
} else {
if (!chapter.isDownloaded) {
loadOnlinePages(chapter)
}
}
private fun loadDownloadedPages(chapter: ReaderChapter) {
val chapterDir = downloadManager.getAbsoluteChapterDirectory(source, manga, chapter)
subscriptions += Observable.from(chapter.pages!!)
.flatMap { downloadManager.getDownloadedImage(it, chapterDir) }
.subscribeOn(Schedulers.io())
.subscribe()
}
private fun loadOnlinePages(chapter: ReaderChapter) {
chapter.pages?.let { pages ->
val startPage = chapter.requestedPage

View File

@@ -5,7 +5,6 @@ import android.content.Intent
import android.content.pm.ActivityInfo
import android.content.res.Configuration
import android.graphics.Color
import android.net.Uri
import android.os.Build
import android.os.Build.VERSION_CODES.KITKAT
import android.os.Bundle
@@ -265,7 +264,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
val activePage = pages.getOrElse(chapter.requestedPage) { pages.first() }
viewer?.onPageListReady(chapter, activePage)
setActiveChapter(chapter, activePage.pageNumber)
setActiveChapter(chapter, activePage.index)
}
fun onEnterChapter(chapter: ReaderChapter, currentPage: Int) {
@@ -332,7 +331,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
fun onPageChanged(page: Page) {
presenter.onPageChanged(page)
val pageNumber = page.pageNumber + 1
val pageNumber = page.index + 1
val pageCount = page.chapter.pages!!.size
page_number.text = "$pageNumber/$pageCount"
if (page_seekbar.rotation != 180f) {
@@ -340,7 +339,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
} else {
right_page_text.text = "$pageNumber"
}
page_seekbar.progress = page.pageNumber
page_seekbar.progress = page.index
}
fun gotoPageInCurrentChapter(pageIndex: Int) {
@@ -481,7 +480,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
val shareIntent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_STREAM, Uri.parse(page.imagePath))
putExtra(Intent.EXTRA_STREAM, page.uri)
flags = Intent.FLAG_ACTIVITY_NEW_TASK
type = "image/jpeg"
}

View File

@@ -29,7 +29,6 @@ import rx.schedulers.Schedulers
import timber.log.Timber
import uy.kohesive.injekt.injectLazy
import java.io.File
import java.io.IOException
import java.util.*
/**
@@ -98,15 +97,6 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
*/
private val source by lazy { sourceManager.get(manga.source)!! }
/**
* Directory of pictures
*/
private val pictureDirectory: String by lazy {
Environment.getExternalStorageDirectory().absolutePath + File.separator +
Environment.DIRECTORY_PICTURES + File.separator +
context.getString(R.string.app_name) + File.separator
}
/**
* Chapter list for the active manga. It's retrieved lazily and should be accessed for the first
* time in a background thread to avoid blocking the UI.
@@ -351,9 +341,9 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
fun retryPage(page: Page?) {
if (page != null && source is OnlineSource) {
page.status = Page.QUEUE
val path = page.imagePath
if (!path.isNullOrEmpty() && !page.chapter.isDownloaded) {
chapterCache.removeFileFromCache(File(path).name)
val uri = page.uri
if (uri != null && !page.chapter.isDownloaded) {
chapterCache.removeFileFromCache(uri.encodedPath.substringAfterLast('/'))
}
loader.retryPage(page)
}
@@ -370,27 +360,27 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
val pages = chapter.pages ?: return
Observable.fromCallable {
// Chapters with 1 page don't trigger page changes, so mark them as read.
if (pages.size == 1) {
chapter.read = true
}
// Cache current page list progress for online chapters to allow a faster reopen
if (!chapter.isDownloaded) {
source.let { if (it is OnlineSource) it.savePageList(chapter, pages) }
}
if (chapter.read) {
val removeAfterReadSlots = prefs.removeAfterReadSlots()
when (removeAfterReadSlots) {
// Setting disabled
-1 -> { /**Empty function**/ }
// Remove current read chapter
0 -> deleteChapter(chapter, manga)
// Remove previous chapter specified by user in settings.
else -> getAdjacentChaptersStrategy(chapter, removeAfterReadSlots)
.first?.let { deleteChapter(it, manga) }
try {
if (chapter.read) {
val removeAfterReadSlots = prefs.removeAfterReadSlots()
when (removeAfterReadSlots) {
// Setting disabled
-1 -> { /* Empty function */ }
// Remove current read chapter
0 -> deleteChapter(chapter, manga)
// Remove previous chapter specified by user in settings.
else -> getAdjacentChaptersStrategy(chapter, removeAfterReadSlots)
.first?.let { deleteChapter(it, manga) }
}
}
} catch (error: Exception) {
// TODO find out why it crashes
Timber.e(error)
}
db.updateChapterProgress(chapter).executeAsBlocking()
@@ -414,7 +404,7 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
*/
fun onPageChanged(page: Page) {
val chapter = page.chapter
chapter.last_page_read = page.pageNumber
chapter.last_page_read = page.index
if (chapter.pages!!.last() === page) {
chapter.read = true
}
@@ -537,7 +527,8 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
try {
if (manga.favorite) {
if (manga.thumbnail_url != null) {
coverCache.copyToCache(manga.thumbnail_url!!, File(page.imagePath).inputStream())
val input = context.contentResolver.openInputStream(page.uri)
coverCache.copyToCache(manga.thumbnail_url!!, input)
context.toast(R.string.cover_updated)
} else {
throw Exception("Image url not found")
@@ -552,40 +543,47 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
}
/**
* Save page to local storage
* @throws IOException
* Save page to local storage.
*/
@Throws(IOException::class)
internal fun savePage(page: Page) {
if (page.status != Page.READY)
return
// Used to show image notification
// Used to show image notification.
val imageNotifier = ImageNotifier(context)
// Location of image file.
val inputFile = File(page.imagePath)
// File where the image will be saved.
val destFile = File(pictureDirectory, manga.title + " - " + chapter.name +
" - " + downloadManager.getImageFilename(page))
//Remove the notification if already exist (user feedback)
// Remove the notification if it already exists (user feedback).
imageNotifier.onClear()
if (inputFile.exists()) {
// Copy file
Observable.fromCallable { inputFile.copyTo(destFile, true) }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
// Show notification
imageNotifier.onComplete(it)
},
{ error ->
Timber.e(error)
imageNotifier.onError(error.message)
})
}
// Pictures directory.
val pictureDirectory = Environment.getExternalStorageDirectory().absolutePath +
File.separator + Environment.DIRECTORY_PICTURES +
File.separator + context.getString(R.string.app_name)
// Copy file in background.
Observable
.fromCallable {
// File where the image will be saved.
val destDir = File(pictureDirectory)
destDir.mkdirs()
val destFile = File(destDir, manga.title + " - " + chapter.name +
" - " + (page.index + 1))
// Location of image file.
context.contentResolver.openInputStream(page.uri).use { input ->
destFile.outputStream().use { output ->
input.copyTo(output)
}
}
imageNotifier.onComplete(destFile)
}
.subscribeOn(Schedulers.io())
.subscribe({},
{ error ->
Timber.e(error)
imageNotifier.onError(error.message)
})
}
}

View File

@@ -2,12 +2,9 @@ package eu.kanade.tachiyomi.ui.reader.notification
import android.content.Context
import android.graphics.Bitmap
import android.media.Image
import android.support.v4.app.NotificationCompat
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.request.animation.GlideAnimation
import com.bumptech.glide.request.target.SimpleTarget
import eu.kanade.tachiyomi.Constants
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.notificationManager
@@ -29,24 +26,25 @@ class ImageNotifier(private val context: Context) {
get() = Constants.NOTIFICATION_DOWNLOAD_IMAGE_ID
/**
* Called when image download/copy is complete
* @param file image file containing downloaded page image
* Called when image download/copy is complete. This method must be called in a background
* thread.
*
* @param file image file containing downloaded page image.
*/
fun onComplete(file: File) {
val bitmap = Glide.with(context)
.load(file)
.asBitmap()
.diskCacheStrategy(DiskCacheStrategy.NONE)
.skipMemoryCache(true)
.into(720, 1280)
.get()
Glide.with(context).load(file).asBitmap().diskCacheStrategy(DiskCacheStrategy.NONE).skipMemoryCache(true).into(object : SimpleTarget<Bitmap>(720, 1280) {
/**
* The method that will be called when the resource load has finished.
* @param resource the loaded resource.
*/
override fun onResourceReady(resource: Bitmap?, glideAnimation: GlideAnimation<in Bitmap>?) {
if (resource!= null){
showCompleteNotification(file, resource)
}else{
onError(null)
}
}
})
if (bitmap != null) {
showCompleteNotification(file, bitmap)
} else {
onError(null)
}
}
private fun showCompleteNotification(file: File, image: Bitmap) {
@@ -75,7 +73,7 @@ class ImageNotifier(private val context: Context) {
}
/**
* Clears the notification message
* Clears the notification message.
*/
fun onClear() {
context.notificationManager.cancel(notificationId)
@@ -88,8 +86,8 @@ class ImageNotifier(private val context: Context) {
/**
* Called on error while downloading image
* @param error string containing error information
* Called on error while downloading image.
* @param error string containing error information.
*/
fun onError(error: String?) {
// Create notification

View File

@@ -95,7 +95,7 @@ abstract class BaseReader : BaseFragment() {
// Active chapter has changed.
if (oldChapter.id != newChapter.id) {
readerActivity.onEnterChapter(newPage.chapter, newPage.pageNumber)
readerActivity.onEnterChapter(newPage.chapter, newPage.index)
}
// Request next chapter only when the conditions are met.
if (pages.size - position < 5 && chapters.last().id == newChapter.id
@@ -125,7 +125,7 @@ abstract class BaseReader : BaseFragment() {
*/
fun getPageIndex(search: Page): Int {
for ((index, page) in pages.withIndex()) {
if (page.pageNumber == search.pageNumber && page.chapter.id == search.chapter.id) {
if (page.index == search.index && page.chapter.id == search.chapter.id) {
return index
}
}

View File

@@ -2,12 +2,14 @@ package eu.kanade.tachiyomi.ui.reader.viewer.pager
import android.content.Context
import android.graphics.PointF
import android.os.Build
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.widget.FrameLayout
import com.davemorrissey.labs.subscaleview.ImageSource
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
@@ -208,13 +210,25 @@ class PageView @JvmOverloads constructor(context: Context, attrs: AttributeSet?
* Called when the page is ready.
*/
private fun setImage() {
val path = page.imagePath
if (path != null && File(path).exists()) {
progress_text.visibility = View.INVISIBLE
image_view.setImage(ImageSource.uri(path))
} else {
val uri = page.uri
if (uri == null) {
page.status = Page.ERROR
return
}
val file = if (Build.VERSION.SDK_INT < 21 || UniFile.isFileUri(uri)) {
UniFile.fromFile(File(uri.path))
} else {
// Tree uri returns the root folder
UniFile.fromSingleUri(context, uri)
}!!
if (!file.exists()) {
page.status = Page.ERROR
return
}
progress_text.visibility = View.INVISIBLE
image_view.setImage(ImageSource.uri(file.uri))
}
/**

View File

@@ -1,11 +1,13 @@
package eu.kanade.tachiyomi.ui.reader.viewer.webtoon
import android.os.Build
import android.support.v7.widget.RecyclerView
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import com.davemorrissey.labs.subscaleview.ImageSource
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
@@ -242,14 +244,26 @@ class WebtoonHolder(private val view: View, private val adapter: WebtoonAdapter)
* Called when the page is ready.
*/
private fun setImage() = with(view) {
val path = page?.imagePath
if (path != null && File(path).exists()) {
progress_text.visibility = View.INVISIBLE
image_view.visibility = View.VISIBLE
image_view.setImage(ImageSource.uri(path))
} else {
val uri = page?.uri
if (uri == null) {
page?.status = Page.ERROR
return
}
val file = if (Build.VERSION.SDK_INT < 21 || UniFile.isFileUri(uri)) {
UniFile.fromFile(File(uri.path))
} else {
// Tree uri returns the root folder
UniFile.fromSingleUri(context, uri)
}!!
if (!file.exists()) {
page?.status = Page.ERROR
return
}
progress_text.visibility = View.INVISIBLE
image_view.visibility = View.VISIBLE
image_view.setImage(ImageSource.uri(file.uri))
}
/**

View File

@@ -116,7 +116,7 @@ class WebtoonReader : BaseReader() {
}
override fun onSaveInstanceState(outState: Bundle) {
val savedPosition = pages.getOrNull(layoutManager.findFirstVisibleItemPosition())?.pageNumber ?: 0
val savedPosition = pages.getOrNull(layoutManager.findFirstVisibleItemPosition())?.index ?: 0
outState.putInt(SAVED_POSITION, savedPosition)
super.onSaveInstanceState(outState)
}
@@ -163,7 +163,7 @@ class WebtoonReader : BaseReader() {
* @param currentPage the initial page to display.
*/
override fun onChapterSet(chapter: ReaderChapter, currentPage: Page) {
this.currentPage = currentPage.pageNumber
this.currentPage = currentPage.index
// Make sure the view is already initialized.
if (view != null) {

View File

@@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.ui.recent_updates
import android.os.Bundle
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.MangaChapter
import eu.kanade.tachiyomi.data.download.DownloadManager
@@ -97,7 +98,10 @@ class RecentChaptersPresenter : BasePresenter<RecentChaptersFragment>() {
.map { mangaChapters ->
mangaChapters.map { it.toModel() }
}
.doOnNext { chapters = it }
.doOnNext {
setDownloadedChapters(it)
chapters = it
}
// Group chapters by the date they were fetched on a ordered map.
.flatMap { recentItems ->
Observable.from(recentItems)
@@ -142,18 +146,29 @@ class RecentChaptersPresenter : BasePresenter<RecentChaptersFragment>() {
// downloaded and assign it to the status.
if (download != null) {
model.download = download
} else {
// Get source of chapter.
val source = sourceManager.get(manga.source)!!
model.status = if (downloadManager.isChapterDownloaded(source, manga, chapter))
Download.DOWNLOADED
else
Download.NOT_DOWNLOADED
}
return model
}
/**
* Finds and assigns the list of downloaded chapters.
*
* @param chapters the list of chapter from the database.
*/
private fun setDownloadedChapters(chapters: List<RecentChapter>) {
val cachedDirs = mutableMapOf<Long, UniFile?>()
chapters.forEach { chapter ->
val manga = chapter.manga
val mangaDir = cachedDirs.getOrPut(manga.id!!)
{ downloadManager.findMangaDir(sourceManager.get(manga.source)!!, manga) }
if (mangaDir?.findFile(downloadManager.getChapterDirName(chapter)) != null) {
chapter.status = Download.DOWNLOADED
}
}
}
/**
* Update status of chapters.
* @param download download object containing progress.
@@ -207,10 +222,6 @@ class RecentChaptersPresenter : BasePresenter<RecentChaptersFragment>() {
* @param chapters list of chapters
*/
fun deleteChapters(chapters: List<RecentChapter>) {
val wasRunning = downloadManager.isRunning
if (wasRunning) {
DownloadService.stop(context)
}
Observable.from(chapters)
.doOnNext { deleteChapter(it) }
.toList()
@@ -218,9 +229,6 @@ class RecentChaptersPresenter : BasePresenter<RecentChaptersFragment>() {
.observeOn(AndroidSchedulers.mainThread())
.subscribeFirst({ view, result ->
view.onChaptersDeleted()
if (wasRunning) {
DownloadService.start(context)
}
}, { view, error ->
view.onChaptersDeletedError(error)
})
@@ -253,7 +261,7 @@ class RecentChaptersPresenter : BasePresenter<RecentChaptersFragment>() {
*/
private fun deleteChapter(chapter: RecentChapter) {
val source = sourceManager.get(chapter.manga.source) ?: return
downloadManager.queue.del(chapter)
downloadManager.queue.remove(chapter)
downloadManager.deleteChapter(source, chapter.manga, chapter)
chapter.status = Download.NOT_DOWNLOADED
chapter.download = null

View File

@@ -2,6 +2,8 @@ package eu.kanade.tachiyomi.ui.setting
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.support.v4.content.ContextCompat
@@ -11,6 +13,7 @@ import android.support.v7.widget.RecyclerView
import android.view.View
import android.view.ViewGroup
import com.afollestad.materialdialogs.MaterialDialog
import com.hippo.unifile.UniFile
import com.nononsenseapps.filepicker.AbstractFilePickerFragment
import com.nononsenseapps.filepicker.FilePickerActivity
import com.nononsenseapps.filepicker.FilePickerFragment
@@ -26,7 +29,8 @@ import java.io.File
class SettingsDownloadsFragment : SettingsFragment() {
companion object {
val DOWNLOAD_DIR_CODE = 103
const val DOWNLOAD_DIR_PRE_L = 103
const val DOWNLOAD_DIR_L = 104
fun newInstance(rootKey: String): SettingsDownloadsFragment {
val args = Bundle()
@@ -45,24 +49,30 @@ class SettingsDownloadsFragment : SettingsFragment() {
downloadDirPref.setOnPreferenceClickListener {
val currentDir = preferences.downloadsDirectory().getOrDefault()
val externalDirs = getExternalFilesDirs() + getString(R.string.custom_dir)
val selectedIndex = externalDirs.indexOf(File(currentDir))
val externalDirs = getExternalFilesDirs() + File(getString(R.string.custom_dir))
val selectedIndex = externalDirs.map(File::toString).indexOfFirst { it in currentDir }
MaterialDialog.Builder(activity)
.items(externalDirs)
.itemsCallbackSingleChoice(selectedIndex, { dialog, view, which, text ->
if (which == externalDirs.lastIndex) {
// Custom dir selected, open directory selector
val i = Intent(activity, CustomLayoutPickerActivity::class.java)
i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false)
i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, true)
i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR)
i.putExtra(FilePickerActivity.EXTRA_START_PATH, currentDir)
if (Build.VERSION.SDK_INT < 21) {
// Custom dir selected, open directory selector
val i = Intent(activity, CustomLayoutPickerActivity::class.java)
i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false)
i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, true)
i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR)
i.putExtra(FilePickerActivity.EXTRA_START_PATH, currentDir)
startActivityForResult(i, DOWNLOAD_DIR_CODE)
startActivityForResult(i, DOWNLOAD_DIR_PRE_L)
} else {
val i = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
startActivityForResult(i, DOWNLOAD_DIR_L)
}
} else {
// One of the predefined folders was selected
preferences.downloadsDirectory().set(text.toString())
val path = Uri.fromFile(File(text.toString()))
preferences.downloadsDirectory().set(path.toString())
}
true
})
@@ -72,7 +82,15 @@ class SettingsDownloadsFragment : SettingsFragment() {
}
subscriptions += preferences.downloadsDirectory().asObservable()
.subscribe { downloadDirPref.summary = it }
.subscribe { path ->
downloadDirPref.summary = path
// Don't display downloaded chapters in gallery apps creating a ".nomedia" file.
val dir = UniFile.fromUri(context, Uri.parse(path))
if (dir != null && dir.exists()) {
dir.createFile(".nomedia")
}
}
}
fun getExternalFilesDirs(): List<File> {
@@ -85,8 +103,22 @@ class SettingsDownloadsFragment : SettingsFragment() {
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (data != null && requestCode == DOWNLOAD_DIR_CODE && resultCode == Activity.RESULT_OK) {
preferences.downloadsDirectory().set(data.data.path)
when (requestCode) {
DOWNLOAD_DIR_PRE_L -> if (data != null && resultCode == Activity.RESULT_OK) {
val uri = Uri.fromFile(File(data.data.path))
preferences.downloadsDirectory().set(uri.toString())
}
DOWNLOAD_DIR_L -> if (data != null && resultCode == Activity.RESULT_OK) {
val uri = data.data
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
@Suppress("NewApi")
context.contentResolver.takePersistableUriPermission(uri, flags)
val file = UniFile.fromTreeUri(context, uri)
preferences.downloadsDirectory().set(file.uri.toString())
}
}
}