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.updater.UpdateNotificationReceiver"/>
<receiver <receiver android:name=".data.library.LibraryUpdateService$CancelUpdateReceiver" />
android:name=".data.library.LibraryUpdateService$CancelUpdateReceiver">
</receiver> <receiver android:name=".data.download.ImageNotificationReceiver" />
<meta-data <meta-data
android:name="eu.kanade.tachiyomi.data.glide.AppGlideModule" 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_UPDATER_ID = 2
const val NOTIFICATION_DOWNLOAD_CHAPTER_ID = 3 const val NOTIFICATION_DOWNLOAD_CHAPTER_ID = 3
const val NOTIFICATION_DOWNLOAD_CHAPTER_ERROR_ID = 4 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 // 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 url = page.imageUrl
val number = String.format("%03d", page.pageNumber + 1) 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 url = source.mangaDetailsRequest(presenter.manga).url().toString()
val sharingIntent = Intent(Intent.ACTION_SEND).apply { val sharingIntent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain" 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)) 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) { } catch (e: Exception) {
context.toast(e.message) context.toast(e.message)
} }

View File

@ -145,6 +145,8 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
when (item.itemId) { when (item.itemId) {
R.id.action_settings -> ReaderSettingsDialog().show(supportFragmentManager, "settings") R.id.action_settings -> ReaderSettingsDialog().show(supportFragmentManager, "settings")
R.id.action_custom_filter -> ReaderCustomFilterDialog().show(supportFragmentManager, "filter") 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) else -> return super.onOptionsItemSelected(item)
} }
return true return true

View File

@ -1,15 +1,23 @@
package eu.kanade.tachiyomi.ui.reader package eu.kanade.tachiyomi.ui.reader
import android.os.Bundle 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.ChapterCache
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.History import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaSync import eu.kanade.tachiyomi.data.database.models.MangaSync
import eu.kanade.tachiyomi.data.download.DownloadManager 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.MangaSyncManager
import eu.kanade.tachiyomi.data.mangasync.UpdateMangaSyncService 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.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.source.SourceManager import eu.kanade.tachiyomi.data.source.SourceManager
import eu.kanade.tachiyomi.data.source.model.Page 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.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.RetryWithDelay import eu.kanade.tachiyomi.util.RetryWithDelay
import eu.kanade.tachiyomi.util.SharedData import eu.kanade.tachiyomi.util.SharedData
import eu.kanade.tachiyomi.util.saveTo
import eu.kanade.tachiyomi.util.toast
import rx.Observable import rx.Observable
import rx.Subscription import rx.Subscription
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
@ -24,6 +34,8 @@ import rx.schedulers.Schedulers
import timber.log.Timber import timber.log.Timber
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.File import java.io.File
import java.io.IOException
import java.io.InputStream
import java.util.* import java.util.*
/** /**
@ -31,6 +43,11 @@ import java.util.*
*/ */
class ReaderPresenter : BasePresenter<ReaderActivity>() { class ReaderPresenter : BasePresenter<ReaderActivity>() {
/**
* Network helper
*/
private val network: NetworkHelper by injectLazy()
/** /**
* Preferences. * Preferences.
*/ */
@ -61,6 +78,11 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
*/ */
val chapterCache: ChapterCache by injectLazy() val chapterCache: ChapterCache by injectLazy()
/**
* Cover cache.
*/
val coverCache: CoverCache by injectLazy()
/** /**
* Manga being read. * Manga being read.
*/ */
@ -88,6 +110,20 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
*/ */
private val source by lazy { sourceManager.get(manga.source)!! } 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 * 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. * time in a background thread to avoid blocking the UI.
@ -365,7 +401,9 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
val removeAfterReadSlots = prefs.removeAfterReadSlots() val removeAfterReadSlots = prefs.removeAfterReadSlots()
when (removeAfterReadSlots) { when (removeAfterReadSlots) {
// Setting disabled // Setting disabled
-1 -> { /**Empty function**/ } -1 -> {
/**Empty function**/
}
// Remove current read chapter // Remove current read chapter
0 -> deleteChapter(chapter, manga) 0 -> deleteChapter(chapter, manga)
// Remove previous chapter specified by user in settings. // Remove previous chapter specified by user in settings.
@ -508,4 +546,87 @@ class ReaderPresenter : BasePresenter<ReaderActivity>() {
db.insertManga(manga).executeAsBlocking() 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_status_label">Status</string>
<string name="manga_info_source_label">Source</string> <string name="manga_info_source_label">Source</string>
<string name="manga_info_genres_label">Genres</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="share_text">Check out %1$s! at %2$s</string>
<string name="circular_icon">Circular icon</string> <string name="circular_icon">Circular icon</string>
<string name="rounded_icon">Rounded icon</string> <string name="rounded_icon">Rounded icon</string>
@ -267,10 +266,18 @@
<string name="status">Status</string> <string name="status">Status</string>
<string name="chapters">Chapters</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 --> <!-- 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_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> <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 --> <!-- Reader activity -->
<string name="custom_filter">Custom filter</string> <string name="custom_filter">Custom filter</string>