mirror of
https://github.com/mihonapp/mihon.git
synced 2025-11-13 12:38:58 +01:00
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:
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user