1
0
mirror of https://github.com/mihonapp/mihon.git synced 2025-08-11 09:11:32 +02:00

Added option to download page or set page as cover ()

* Added option to download page or set page as cover

* Removed network call now copies from page image

* Format fix + notification feedback

* Added code to prevent OutOfMemory error.  Made notification optional. Can now save image on long press. Bug fixes

* Now uses glide for notification

* Fixed webtoon page

* Fixes + API 16 support

* fixes

* Fixed API 24 FileProvider error

* Added page.ready check

* Indention
This commit is contained in:
inorichi
2016-11-14 20:48:34 +01:00
committed by GitHub
32 changed files with 403 additions and 24 deletions

@@ -5,4 +5,5 @@ object Constants {
const val NOTIFICATION_UPDATER_ID = 2
const val NOTIFICATION_DOWNLOAD_CHAPTER_ID = 3
const val NOTIFICATION_DOWNLOAD_CHAPTER_ERROR_ID = 4
const val NOTIFICATION_DOWNLOAD_IMAGE_ID = 5
}

@@ -297,7 +297,7 @@ class DownloadManager(
}
// Get the filename for an image given the page
private fun getImageFilename(page: Page): String {
fun getImageFilename(page: Page): String {
val url = page.imageUrl
val number = String.format("%03d", page.pageNumber + 1)

@@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
import eu.kanade.tachiyomi.util.notificationManager
import eu.kanade.tachiyomi.util.toast
/**
* DownloadNotifier is used to show notifications when downloading one or multiple chapters.
@@ -47,9 +48,8 @@ class DownloadNotifier(private val context: Context) {
* @param queue the queue containing downloads.
*/
internal fun onProgressChange(queue: DownloadQueue) {
if (multipleDownloadThreads) {
if (multipleDownloadThreads)
doOnProgressChange(null, queue)
}
}
/**
@@ -60,9 +60,8 @@ class DownloadNotifier(private val context: Context) {
* @param queue the queue containing downloads
*/
internal fun onProgressChange(download: Download, queue: DownloadQueue) {
if (!multipleDownloadThreads) {
if (!multipleDownloadThreads)
doOnProgressChange(download, queue)
}
}
/**
@@ -86,7 +85,7 @@ class DownloadNotifier(private val context: Context) {
}
// Create notification
with (notificationBuilder) {
with(notificationBuilder) {
// Check if icon needs refresh
if (!isDownloading) {
setSmallIcon(android.R.drawable.stat_sys_download)
@@ -165,7 +164,6 @@ class DownloadNotifier(private val context: Context) {
setProgress(0, 0, false)
}
context.notificationManager.notify(Constants.NOTIFICATION_DOWNLOAD_CHAPTER_ERROR_ID, notificationBuilder.build())
// Reset download information
onClear()
isDownloading = false

@@ -184,10 +184,9 @@ class MangaInfoFragment : BaseRxFragment<MangaInfoPresenter>() {
val url = source.mangaDetailsRequest(presenter.manga).url().toString()
val sharingIntent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
putExtra(android.content.Intent.EXTRA_SUBJECT, presenter.manga.title)
putExtra(android.content.Intent.EXTRA_TEXT, resources.getString(R.string.share_text, presenter.manga.title, url))
}
startActivity(Intent.createChooser(sharingIntent, resources.getText(R.string.share_subject)))
startActivity(Intent.createChooser(sharingIntent, resources.getText(R.string.action_share)))
} catch (e: Exception) {
context.toast(e.message)
}

@@ -5,6 +5,7 @@ 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
@@ -224,6 +225,20 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
toast(error.message)
}
fun onLongPress(page: Page) {
MaterialDialog.Builder(this)
.title(getString(R.string.options))
.items(R.array.reader_image_options)
.itemsIds(R.array.reader_image_options_values)
.itemsCallback { materialDialog, view, i, charSequence ->
when (i) {
0 -> presenter.setCover(page)
1 -> shareImage(page)
2 -> presenter.savePage(page)
}
}.show()
}
fun onChapterAppendError() {
// Ignore
}
@@ -455,6 +470,24 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
}
}
/**
* Start a share intent that lets user share image
*
* @param page page object containing image information.
*/
fun shareImage(page: Page) {
if (page.status != Page.READY)
return
val shareIntent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_STREAM, Uri.parse(page.imagePath))
flags = Intent.FLAG_ACTIVITY_NEW_TASK
type = "image/jpeg"
}
startActivity(Intent.createChooser(shareIntent, resources.getText(R.string.action_share)))
}
/**
* Sets the brightness of the screen. Range is [-75, 100].
* From -75 to -1 a semi-transparent black view is shown at the top with the minimum brightness.

@@ -1,7 +1,10 @@
package eu.kanade.tachiyomi.ui.reader
import android.os.Bundle
import android.os.Environment
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.History
@@ -15,8 +18,10 @@ import eu.kanade.tachiyomi.data.source.SourceManager
import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.data.source.online.OnlineSource
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.ui.reader.notification.ImageNotifier
import eu.kanade.tachiyomi.util.RetryWithDelay
import eu.kanade.tachiyomi.util.SharedData
import eu.kanade.tachiyomi.util.toast
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
@@ -24,13 +29,13 @@ import rx.schedulers.Schedulers
import timber.log.Timber
import uy.kohesive.injekt.injectLazy
import java.io.File
import java.io.IOException
import java.util.*
/**
* Presenter of [ReaderActivity].
*/
class ReaderPresenter : BasePresenter<ReaderActivity>() {
/**
* Preferences.
*/
@@ -61,6 +66,11 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
*/
val chapterCache: ChapterCache by injectLazy()
/**
* Cover cache.
*/
val coverCache: CoverCache by injectLazy()
/**
* Manga being read.
*/
@@ -88,6 +98,15 @@ 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.
@@ -364,11 +383,11 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
if (chapter.read) {
val removeAfterReadSlots = prefs.removeAfterReadSlots()
when (removeAfterReadSlots) {
// Setting disabled
// Setting disabled
-1 -> { /**Empty function**/ }
// Remove current read chapter
// Remove current read chapter
0 -> deleteChapter(chapter, manga)
// Remove previous chapter specified by user in settings.
// Remove previous chapter specified by user in settings.
else -> getAdjacentChaptersStrategy(chapter, removeAfterReadSlots)
.first?.let { deleteChapter(it, manga) }
}
@@ -384,8 +403,8 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
Timber.e(error)
}
}
.subscribeOn(Schedulers.io())
.subscribe()
.subscribeOn(Schedulers.io())
.subscribe()
}
/**
@@ -508,4 +527,65 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
db.insertManga(manga).executeAsBlocking()
}
/**
* Update cover with page file.
*/
internal fun setCover(page: Page) {
if (page.status != Page.READY)
return
try {
if (manga.favorite) {
if (manga.thumbnail_url != null) {
coverCache.copyToCache(manga.thumbnail_url!!, File(page.imagePath).inputStream())
context.toast(R.string.cover_updated)
} else {
throw Exception("Image url not found")
}
} else {
context.toast(R.string.notification_first_add_to_library)
}
} catch (error: Exception) {
context.toast(R.string.notification_cover_update_failed)
Timber.e(error)
}
}
/**
* Save page to local storage
* @throws IOException
*/
@Throws(IOException::class)
internal fun savePage(page: Page) {
if (page.status != Page.READY)
return
// 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)
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)
})
}
}
}

@@ -0,0 +1,109 @@
package eu.kanade.tachiyomi.ui.reader.notification
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.support.v4.content.FileProvider
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.notificationManager
import java.io.File
/**
* The BroadcastReceiver of [ImageNotifier]
* Intent calls should be made from this class.
*/
class ImageNotificationReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
ACTION_SHARE_IMAGE -> {
shareImage(context, intent.getStringExtra(EXTRA_FILE_LOCATION))
context.notificationManager.cancel(intent.getIntExtra(NOTIFICATION_ID, 5))
}
ACTION_SHOW_IMAGE ->
showImage(context, intent.getStringExtra(EXTRA_FILE_LOCATION))
ACTION_DELETE_IMAGE -> {
deleteImage(intent.getStringExtra(EXTRA_FILE_LOCATION))
context.notificationManager.cancel(intent.getIntExtra(NOTIFICATION_ID, 5))
}
}
}
/**
* Called to delete image
* @param path path of file
*/
private fun deleteImage(path: String) {
val file = File(path)
if (file.exists()) file.delete()
}
/**
* Called to start share intent to share image
* @param context context of application
* @param path path of file
*/
private fun shareImage(context: Context, path: String) {
val shareIntent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_STREAM, Uri.parse(path))
type = "image/jpeg"
}
context.startActivity(Intent.createChooser(shareIntent, context.resources.getText(R.string.action_share))
.apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK })
}
/**
* Called to show image in gallery application
* @param context context of application
* @param path path of file
*/
private fun showImage(context: Context, path: String) {
val intent = Intent().apply {
action = Intent.ACTION_VIEW
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
val uri = FileProvider.getUriForFile(context,"eu.kanade.tachiyomi.provider",File(path))
setDataAndType(uri, "image/*")
}
context.startActivity(intent)
}
companion object {
private const val ACTION_SHARE_IMAGE = "eu.kanade.SHARE_IMAGE"
private const val ACTION_SHOW_IMAGE = "eu.kanade.SHOW_IMAGE"
private const val ACTION_DELETE_IMAGE = "eu.kanade.DELETE_IMAGE"
private const val EXTRA_FILE_LOCATION = "file_location"
private const val NOTIFICATION_ID = "notification_id"
internal fun shareImageIntent(context: Context, path: String, notificationId: Int): PendingIntent {
val intent = Intent(context, ImageNotificationReceiver::class.java).apply {
action = ACTION_SHARE_IMAGE
putExtra(EXTRA_FILE_LOCATION, path)
putExtra(NOTIFICATION_ID, notificationId)
}
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
internal fun showImageIntent(context: Context, path: String): PendingIntent {
val intent = Intent(context, ImageNotificationReceiver::class.java).apply {
action = ACTION_SHOW_IMAGE
putExtra(EXTRA_FILE_LOCATION, path)
}
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
internal fun deleteImageIntent(context: Context, path: String, notificationId: Int): PendingIntent {
val intent = Intent(context, ImageNotificationReceiver::class.java).apply {
action = ACTION_DELETE_IMAGE
putExtra(EXTRA_FILE_LOCATION, path)
putExtra(NOTIFICATION_ID, notificationId)
}
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
}
}

@@ -0,0 +1,104 @@
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
import java.io.File
/**
* Class used to show BigPictureStyle notifications
*/
class ImageNotifier(private val context: Context) {
/**
* Notification builder.
*/
private val notificationBuilder = NotificationCompat.Builder(context)
/**
* Id of the notification.
*/
private val notificationId: Int
get() = Constants.NOTIFICATION_DOWNLOAD_IMAGE_ID
/**
* Called when image download/copy is complete
* @param file image file containing downloaded page image
*/
fun onComplete(file: File) {
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)
}
}
})
}
private fun showCompleteNotification(file: File, image: Bitmap) {
with(notificationBuilder) {
setContentTitle(context.getString(R.string.picture_saved))
setSmallIcon(R.drawable.ic_insert_photo_white_24dp)
setStyle(NotificationCompat.BigPictureStyle().bigPicture(image))
setLargeIcon(image)
setAutoCancel(true)
// Clear old actions if they exist
if (!mActions.isEmpty())
mActions.clear()
setContentIntent(ImageNotificationReceiver.showImageIntent(context, file.absolutePath))
// Share action
addAction(R.drawable.ic_share_grey_24dp,
context.getString(R.string.action_share),
ImageNotificationReceiver.shareImageIntent(context, file.absolutePath, notificationId))
// Delete action
addAction(R.drawable.ic_delete_grey_24dp,
context.getString(R.string.action_delete),
ImageNotificationReceiver.deleteImageIntent(context, file.absolutePath, notificationId))
updateNotification()
}
}
/**
* Clears the notification message
*/
fun onClear() {
context.notificationManager.cancel(notificationId)
}
private fun updateNotification() {
// Displays the progress bar on notification
context.notificationManager.notify(notificationId, notificationBuilder.build())
}
/**
* Called on error while downloading image
* @param error string containing error information
*/
fun onError(error: String?) {
// Create notification
with(notificationBuilder) {
setContentTitle(context.getString(R.string.download_notifier_title_error))
setContentText(error ?: context.getString(R.string.unknown_error))
setSmallIcon(android.R.drawable.ic_menu_report_image)
}
updateNotification()
}
}

@@ -11,6 +11,7 @@ import eu.kanade.tachiyomi.ui.reader.ReaderChapter
import eu.kanade.tachiyomi.ui.reader.viewer.base.BaseReader
import eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal.LeftToRightReader
import eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal.RightToLeftReader
import eu.kanade.tachiyomi.util.toast
import rx.subscriptions.CompositeSubscription
/**
@@ -185,6 +186,16 @@ abstract class PagerReader : BaseReader() {
}
return true
}
override fun onLongPress(e: MotionEvent?) {
if (isAdded) {
val page = adapter.pages.getOrNull(pager.currentItem)
if (page != null)
readerActivity.onLongPress(page)
else
context.toast(getString(R.string.unknown_error))
}
}
})
}

@@ -6,9 +6,11 @@ import android.view.*
import android.view.GestureDetector.SimpleOnGestureListener
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.ui.reader.ReaderChapter
import eu.kanade.tachiyomi.ui.reader.viewer.base.BaseReader
import eu.kanade.tachiyomi.util.toast
import eu.kanade.tachiyomi.widget.PreCachingLayoutManager
import rx.subscriptions.CompositeSubscription
@@ -140,12 +142,23 @@ class WebtoonReader : BaseReader() {
}
return true
}
override fun onLongPress(e: MotionEvent) {
if (isAdded) {
val child = recycler.findChildViewUnder(e.rawX, e.rawY)
val position = recycler.getChildAdapterPosition(child)
val page = adapter.pages?.getOrNull(position)
if (page != null)
readerActivity.onLongPress(page)
else
context.toast(getString(R.string.unknown_error))
}
}
})
}
/**
* Called when a new chapter is set in [BaseReader].
*
* @param chapter the chapter set.
* @param currentPage the initial page to display.
*/
@@ -160,7 +173,6 @@ class WebtoonReader : BaseReader() {
/**
* Called when a chapter is appended in [BaseReader].
*
* @param chapter the chapter appended.
*/
override fun onChapterAppended(chapter: ReaderChapter) {
@@ -184,7 +196,6 @@ class WebtoonReader : BaseReader() {
/**
* Sets the active page.
*
* @param pageNumber the index of the page from [pages].
*/
override fun setActivePage(pageNumber: Int) {