Added option to download page or set page as cover

This commit is contained in:
Bram van de Kerkhof 2016-09-27 00:15:21 +02:00
parent 5b1f4f189b
commit 2991906a85
10 changed files with 367 additions and 14 deletions

View file

@ -86,9 +86,9 @@
<receiver android:name=".data.updater.UpdateNotificationReceiver"/>
<receiver
android:name=".data.library.LibraryUpdateService$CancelUpdateReceiver">
</receiver>
<receiver android:name=".data.library.LibraryUpdateService$CancelUpdateReceiver" />
<receiver android:name=".data.download.ImageNotificationReceiver" />
<meta-data
android:name="eu.kanade.tachiyomi.data.glide.AppGlideModule"

View file

@ -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
}

View file

@ -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)

View file

@ -0,0 +1,90 @@
package eu.kanade.tachiyomi.data.download
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.net.Uri
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.notificationManager
import java.io.File
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))
}
}
}
fun deleteImage(path: String) {
val file = File(path)
if (file.exists()) file.delete()
}
fun shareImage(context: Context, path: String) {
val shareIntent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_STREAM, Uri.parse(path))
flags = Intent.FLAG_ACTIVITY_NEW_TASK
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 })
}
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
setDataAndType(Uri.parse("file://" + path), "image/*")
}
context.startActivity(intent)
}
companion object {
const val ACTION_SHARE_IMAGE = "eu.kanade.SHARE_IMAGE"
const val ACTION_SHOW_IMAGE = "eu.kanade.SHOW_IMAGE"
const val ACTION_DELETE_IMAGE = "eu.kanade.DELETE_IMAGE"
const val EXTRA_FILE_LOCATION = "file_location"
const val NOTIFICATION_ID = "notification_id"
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)
}
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)
}
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)
}
}
}

View file

@ -0,0 +1,124 @@
package eu.kanade.tachiyomi.data.download
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.support.v4.app.NotificationCompat
import eu.kanade.tachiyomi.Constants
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.notificationManager
import java.io.File
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
/**
* Status of download. Used for correct notification icon.
*/
private var isDownloading = false
/**
* Called when download progress changes.
* @param progress progress value in range [0,100]
*/
fun onProgressChange(progress: Int) {
with(notificationBuilder) {
if (!isDownloading) {
setContentTitle(context.getString(R.string.saving_picture))
setSmallIcon(android.R.drawable.stat_sys_download)
setLargeIcon(null)
setStyle(null)
// Clear old actions if they exist
if (!mActions.isEmpty())
mActions.clear()
isDownloading = true
}
setProgress(100, progress, false)
}
// Displays the progress bar on notification
context.notificationManager.notify(notificationId, notificationBuilder.build())
}
/**
* Called when image download is complete
* @param bitmap image file containing downloaded page image
*/
fun onComplete(bitmap: Bitmap, file: File) {
with(notificationBuilder) {
if (isDownloading) {
setProgress(0, 0, false)
isDownloading = false
}
setContentTitle(context.getString(R.string.picture_saved))
setSmallIcon(R.drawable.ic_insert_photo_black_24dp)
setLargeIcon(bitmap)
setStyle(NotificationCompat.BigPictureStyle().bigPicture(bitmap))
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_white_24dp,
context.getString(R.string.action_share),
ImageNotificationReceiver.shareImageIntent(context, file.absolutePath, notificationId))
// Delete action
addAction(R.drawable.ic_delete_white_24dp,
context.getString(R.string.action_delete),
ImageNotificationReceiver.deleteImageIntent(context, file.absolutePath, notificationId))
}
// Displays the progress bar on notification
context.notificationManager.notify(notificationId, notificationBuilder.build())
}
fun onComplete(file: File) {
onComplete(convertToBitmap(file), file)
}
/**
* Clears the notification message
*/
internal fun onClear() {
context.notificationManager.cancel(notificationId)
}
/**
* Called on error while downloading image
* @param error string containing error information
*/
internal fun onError(error: String?) {
// Create notification
with(notificationBuilder) {
setContentTitle(context.getString(R.string.download_notifier_title_error))
setContentText(error ?: context.getString(R.string.download_notifier_unkown_error))
setSmallIcon(android.R.drawable.ic_menu_report_image)
setProgress(0, 0, false)
}
context.notificationManager.notify(notificationId, notificationBuilder.build())
isDownloading = false
}
/**
* Converts file to bitmap
*/
fun convertToBitmap(image: File): Bitmap {
val options = BitmapFactory.Options()
options.inPreferredConfig = Bitmap.Config.ARGB_8888
return BitmapFactory.decodeFile(image.absolutePath, options)
}
}

View file

@ -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)
}

View file

@ -145,6 +145,8 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
when (item.itemId) {
R.id.action_settings -> ReaderSettingsDialog().show(supportFragmentManager, "settings")
R.id.action_custom_filter -> ReaderCustomFilterDialog().show(supportFragmentManager, "filter")
R.id.action_save_page -> presenter.savePage()
R.id.action_set_as_cover -> presenter.setCover()
else -> return super.onOptionsItemSelected(item)
}
return true
@ -393,16 +395,16 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
private fun setRotation(rotation: Int) {
when (rotation) {
// Rotation free
// Rotation free
1 -> requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
// Lock in current rotation
// Lock in current rotation
2 -> {
val currentOrientation = resources.configuration.orientation
setRotation(if (currentOrientation == Configuration.ORIENTATION_PORTRAIT) 3 else 4)
}
// Lock in portrait
// Lock in portrait
3 -> requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT
// Lock in landscape
// Lock in landscape
4 -> requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
}
}

View file

@ -1,15 +1,23 @@
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
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaSync
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.ImageNotifier
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager
import eu.kanade.tachiyomi.data.mangasync.UpdateMangaSyncService
import eu.kanade.tachiyomi.data.network.GET
import eu.kanade.tachiyomi.data.network.NetworkHelper
import eu.kanade.tachiyomi.data.network.ProgressListener
import eu.kanade.tachiyomi.data.network.newCallWithProgress
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.source.SourceManager
import eu.kanade.tachiyomi.data.source.model.Page
@ -17,6 +25,8 @@ import eu.kanade.tachiyomi.data.source.online.OnlineSource
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.RetryWithDelay
import eu.kanade.tachiyomi.util.SharedData
import eu.kanade.tachiyomi.util.saveTo
import eu.kanade.tachiyomi.util.toast
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
@ -24,6 +34,8 @@ import rx.schedulers.Schedulers
import timber.log.Timber
import uy.kohesive.injekt.injectLazy
import java.io.File
import java.io.IOException
import java.io.InputStream
import java.util.*
/**
@ -31,6 +43,11 @@ import java.util.*
*/
class ReaderPresenter : BasePresenter<ReaderActivity>() {
/**
* Network helper
*/
private val network: NetworkHelper by injectLazy()
/**
* Preferences.
*/
@ -61,6 +78,11 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
*/
val chapterCache: ChapterCache by injectLazy()
/**
* Cover cache.
*/
val coverCache: CoverCache by injectLazy()
/**
* Manga being read.
*/
@ -88,6 +110,20 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
*/
private val source by lazy { sourceManager.get(manga.source)!! }
/**
*
*/
val imageNotifier by lazy { ImageNotifier(context) }
/**
* 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.
@ -365,7 +401,9 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
val removeAfterReadSlots = prefs.removeAfterReadSlots()
when (removeAfterReadSlots) {
// Setting disabled
-1 -> { /**Empty function**/ }
-1 -> {
/**Empty function**/
}
// Remove current read chapter
0 -> deleteChapter(chapter, manga)
// Remove previous chapter specified by user in settings.
@ -384,8 +422,8 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
Timber.e(error)
}
}
.subscribeOn(Schedulers.io())
.subscribe()
.subscribeOn(Schedulers.io())
.subscribe()
}
/**
@ -508,4 +546,87 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
db.insertManga(manga).executeAsBlocking()
}
/**
* Update cover with page file.
*/
internal fun setCover() {
chapter.pages?.get(chapter.last_page_read)?.let {
// Update cover to selected file, show error if something went wrong
try {
if (editCoverWithStream(File(it.imagePath).inputStream(), manga)) {
context.toast(R.string.cover_updated)
} else {
throw Exception("Stream copy failed")
}
} catch(e: Exception) {
context.toast(R.string.notification_manga_update_failed)
Timber.e(e.message)
}
}
}
/**
* Called to copy image to cache
* @param inputStream the new cover.
* @param manga the manga edited.
* @return true if the cover is updated, false otherwise
*/
@Throws(IOException::class)
private fun editCoverWithStream(inputStream: InputStream, manga: Manga): Boolean {
if (manga.thumbnail_url != null && manga.favorite) {
coverCache.copyToCache(manga.thumbnail_url!!, inputStream)
return true
}
return false
}
/**
* Save page to local storage
* @throws IOException
*/
@Throws(IOException::class)
internal fun savePage() {
chapter.pages?.get(chapter.last_page_read)?.let { page ->
// File where the image will be saved
val destFile = File(pictureDirectory, manga.title + " - " + chapter.name +
" - " + downloadManager.getImageFilename(page))
if (destFile.exists()) {
imageNotifier.onComplete(destFile)
} else {
// Progress of the download
var savedProgress = 0
val progressListener = object : ProgressListener {
override fun update(bytesRead: Long, contentLength: Long, done: Boolean) {
val progress = (100 * bytesRead / contentLength).toInt()
if (progress > savedProgress) {
savedProgress = progress
imageNotifier.onProgressChange(progress)
}
}
}
// Download and save the image.
Observable.fromCallable { ->
network.client.newCallWithProgress(GET(page.imageUrl!!), progressListener).execute()
}.map {
response ->
if (response.isSuccessful) {
response.body().source().saveTo(destFile)
imageNotifier.onComplete(destFile)
} else {
response.close()
throw Exception("Unsuccessful response")
}
}
.observeOn(AndroidSchedulers.mainThread())
.subscribeOn(Schedulers.io())
.subscribe({}, { error ->
Timber.e(error.message)
imageNotifier.onError(error.message)
})
}
}
}
}

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z" />
</vector>

View file

@ -223,7 +223,6 @@
<string name="manga_info_status_label">Status</string>
<string name="manga_info_source_label">Source</string>
<string name="manga_info_genres_label">Genres</string>
<string name="share_subject">Share…</string>
<string name="share_text">Check out %1$s! at %2$s</string>
<string name="circular_icon">Circular icon</string>
<string name="rounded_icon">Rounded icon</string>
@ -267,10 +266,18 @@
<string name="status">Status</string>
<string name="chapters">Chapters</string>
<!-- Reader Activity -->
<string name="custom_filter">Custom filter</string>
<string name="save_page">Download page</string>
<string name="set_as_cover">Set as cover</string>
<string name="cover_updated">Cover updated</string>
<!-- Dialog remove recently view -->
<string name="dialog_remove_recently_description">This will remove the read date of this chapter. Are you sure?</string>
<string name="dialog_remove_recently_reset">Reset all chapters for this manga</string>
<!-- Image notifier -->
<string name="picture_saved">Picture saved</string>
<string name="saving_picture">Saving picture</string>
<!-- Reader activity -->
<string name="custom_filter">Custom filter</string>